Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/cover_chain_sim.c
T
AskaEth 2bfe1ed796 feat(ncm): 封面死链兜底链 —— 原 picUrl 失败→web 端 song/detail 重取活链→歌单封面兜底
用户调研: 官方旧版 GET music.163.com/api/song/detail/?id=<id>&ids=[<id>] 免登录出
songs[0].album.picUrl/blurPicUrl。本批宿主实锤: 登录态 eapi 给的死链封面, 该 web 接口给不同活链
(死链 yR2VGt7.../10995116862531 -> album.picUrl p2.../6y-UleORIT.../...jpg 活 PNG 146KB)。

实现 (patch_v2/ncm_app/):
- cover_request(url, song_id): 新增第二参曲目 id; g_tgt_song 记录; 新目标重置 g_fb_done。
  player.c 三处传 s.id(专辑封面), playlist.c 传 NULL(歌单封面无兜底)。
- cover_song_detail_pic(song_id): web song/detail 重取 album.picUrl(缺 blurPicUrl);
  ids=[<id>] 的 [ ] URL 编码 %5B/%5D(裸[ ] curl 报 bad range); 带 Referer+桌面UA; ncm_http_request
  (头进 resp->headers, 体进 16KB 栈缓冲) + ncm_json 解析; 只读一次。
- cover_try_fallback(url, song_id): 仅原链确定性失败时触发; g_tgt_url 仍==url(未被换走) + 曲目非空 +
  未兜底 + 同曲 5 分钟冷却(ncm_monotonic_ms) 才下探; 拿到不同新图->记冷却/置 g_fb_done/换目标返 1
  (worker 重试新图); 无新图/冷却中/已兜底/目标被换走->返 0 走歌单封面兜底; 每步日志写清级别。
- worker: kind=1(webp)/kind=2(非图)/确定性4xx 死链分支清目标前先
  if(song[0]&&!fb_done&&cover_try_fallback(url,song)) continue; 瞬时错误仍指数退避不触发。
- 风控: 只在原链失败触发(不新增常规请求); 单曲目最多一次(g_fb_done)+5分钟冷却。

验证 (宿主+qemu, 0 新增真实 CDN 业务请求):
- cover_chain_sim T1-T12 ALL PASS(新增 T11 原链死->song/detail活链->出图+恰1次+song-detail fallback日志;
  T12 兜底也死->留歌单封面+同曲再请求 song/detail 0次(冷却)+cooldown日志)。
- cache_evict/player_flow/window/search/stream sim 全 PASS; core host+mipsel(qemu) PASS;
  build_ncm.sh 0 warning(text 1022210/bss 1362564,+64B); ncm_host UI dump exit=0。
  新 ncm md5 e1f4b7ade834b18c7910b9c3e64bc858 (未部署, 留主会话上卡)。
2026-09-02 11:41:54 +08:00

416 lines
18 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.
/* 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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/stat.h>
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;
}
size_t ncm_strlcat(char *dst, const char *src, size_t sz)
{
size_t dn = strlen(dst), sn = 0;
while (dn + sn + 1 < sz && src[sn]) { dst[dn + sn] = src[sn]; sn++; }
if (sz) dst[dn + sn] = 0;
return dn + sn;
}
int ncm_streq(const char *a, const char *b) { return a && b && strcmp(a, b) == 0; }
int ncm_parse_int(const char *s, long *out) { char *e; long v = strtol(s, &e, 10); if (e == s || *e) return -1; if (out) *out = v; return 0; }
int64_t ncm_monotonic_ms(void) { static int64_t t = 0; return ++t; } /* 单调递增:冷却时差恒 <5min(T12 冷却判定用) */
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/json/403/死链前缀 ---- */
static int g_http_calls;
static int g_sd_calls; /* song/detail 请求计数(死链兜底专用,REPORT §NEW 任务1) */
static int g_fail_next; /* 剩余注入失败次数 */
static int g_delay_ms; /* 模拟慢网络 */
static int g_webp_next; /* 下一次下载返回 WebP 魔数体(200,测「200 解码失败」三分型③) */
static int g_json_next; /* 下一次下载返回 JSON 错误体(200,实测「200 解码失败」真凶=死对象回 JSON) */
static int g_status_next; /* 下一次下载强制返回该 HTTP status(测 403 防盗链;0=200) */
static char g_dead_prefix[128] = ""; /* URL 此前缀 → 返 404(死链注入;兜底链测试用) */
static char g_alt_pic[256] = ""; /* song/detail 响应里给的新 picUrl(兜底链活链/死链用) */
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);
/* 死链兜底:song/detail 请求 → 返 JSON(songs[0].album.picUrl = g_alt_pic),供 cover.c
* cover_song_detail_pic 重取新图(REPORT §NEW 任务1)。 */
if (strstr(req->url, "/api/song/detail/")) {
g_sd_calls++;
int n = snprintf(buf, buf_sz,
"{\"songs\":[{\"album\":{\"picUrl\":\"%s\",\"blurPicUrl\":\"\"}}],\"code\":200}",
g_alt_pic);
if (n < 0 || (size_t)n >= buf_sz) return NCM_ERR_FULL;
memset(resp, 0, sizeof *resp);
resp->status = 200;
resp->body = buf;
resp->body_len = (size_t)n;
resp->content_length = (size_t)n;
resp->has_content_type = 1;
ncm_strlcpy(resp->content_type, "application/json", sizeof resp->content_type);
return NCM_OK;
}
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);
/* 真实 cover.c 会做可解码图魔数判定(REPORT §34:200 但非 JPEG/PNG/GIF 判非图不重试),
* 故 mock body 加 JPEG 魔数 FFD8FF,与 url_hash() 期望同规则。 */
if (n >= 3) { buf[0]=(char)0xFF; buf[1]=(char)0xD8; buf[2]=(char)0xFF; }
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++;
/* 死链注入:URL 前缀命中 → 返 404 无 body(cover.c 判确定性 4xx,走 song/detail 兜底) */
if (g_dead_prefix[0] && strncmp(req->url, g_dead_prefix, strlen(g_dead_prefix)) == 0) {
memset(resp, 0, sizeof *resp);
resp->status = 404;
return NCM_OK;
}
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);
/* JPEG 魔数(cover.c 可解码图判定通过);webp/json 注入优先覆盖各自魔数 */
buf[0]=0xFF; buf[1]=0xD8; buf[2]=0xFF;
if (g_json_next) {
g_json_next = 0;
buf[0]='{'; buf[1]='"'; buf[2]='c'; buf[3]='o'; buf[4]='d'; buf[5]='e';
}
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_COVER_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, '?') ? "&param=" : "?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);
tmp[0]=0xFF; tmp[1]=0xD8; tmp[2]=0xFF; /* 与 stub JPEG 魔数一致 */
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 注入用(未缓存) */
#define URL_G "http://p1.music.126.net/albumG/ttt.jpg" /* JSON 错误体注入用(未缓存) */
#define URL_H "http://p1.music.126.net/dead/album/AAA.jpg" /* 死链原图(song/detail 兜底测试用) */
#define URL_H2 "http://p1.music.126.net/dead/album/BBB.jpg" /* 兜底给到的也死链 alt */
#define URL_LIVE "http://p1.music.126.net/live/SONG.jpg" /* 兜底给到的活链 alt */
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, NULL);
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, NULL);
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, NULL);
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, NULL);
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, NULL);
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, NULL);
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, NULL);
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("== T10: 200+JSON error body (dead origin) -> not an image, no retry ==\n");
int b10 = g_http_calls;
g_json_next = 1;
cover_request(URL_G, NULL);
usleep(800 * 1000);
CHECK(g_http_calls == b10 + 1, "json: single download=%d (no retry)", g_http_calls - b10);
CHECK(cur_cover_hash() == url_hash(URL_C), "json: 旧封面 C 保留(兜底)");
CHECK(strstr(g_last_log, "not an image") != NULL, "log: 'not an image' seen");
printf("== T11: 原链死 -> song/detail 兜底活链 -> 出图 ==\n");
g_fail_next = 0; g_json_next = 0; g_webp_next = 0; g_status_next = 0;
ncm_strlcpy(g_dead_prefix, "http://p1.music.126.net/dead/", sizeof g_dead_prefix);
ncm_strlcpy(g_alt_pic, URL_LIVE, sizeof g_alt_pic);
int sd11 = g_sd_calls;
cover_request(URL_H, "90001");
CHECK(wait_cover_hash(url_hash(URL_LIVE), 6000), "cover shown via song/detail live pic");
CHECK(g_sd_calls == sd11 + 1, "song/detail called once (sd=%d)", g_sd_calls - sd11);
CHECK(cur_cover_hash() == url_hash(URL_LIVE), "cover == song/detail live pic");
CHECK(strstr(g_last_log, "song-detail fallback id=90001") != NULL, "log: song-detail fallback seen");
printf("== T12: 原链死 -> 兜底也死 -> 歌单封面兜底 + 冷却不重试 ==\n");
ncm_strlcpy(g_alt_pic, URL_H2, sizeof g_alt_pic); /* 兜底也给一个死链 URL */
int sd12 = g_sd_calls;
cover_request(URL_H, "90002");
usleep(1500 * 1000); /* 等:URL_H 死 -> song/detail(90002)->URL_H2 -> URL_H2 也 404 -> 清目标 */
CHECK(g_sd_calls == sd12 + 1, "T12 song/detail once (sd=%d)", g_sd_calls - sd12);
CHECK(cur_cover_hash() == url_hash(URL_LIVE), "T12 兜底也死:保留上个封面(URL_LIVE)");
int sd13 = g_sd_calls;
cover_request(URL_H, "90002"); /* 同曲 90002 再请求 → 冷却,不再 song/detail */
usleep(1500 * 1000);
CHECK(g_sd_calls == sd13, "cooldown: no 2nd song/detail (sd=%d)", g_sd_calls - sd13);
CHECK(strstr(g_last_log, "song-detail fallback cooldown") != NULL, "log: cooldown seen");
g_dead_prefix[0] = 0; g_alt_pic[0] = 0;
printf("== T7: null/empty + reset ==\n");
int b7 = g_http_calls;
cover_request(NULL, NULL);
cover_request("", NULL);
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;
}