任务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)。
419 lines
19 KiB
C
419 lines
19 KiB
C
/* 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, '?') ? "¶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);
|
||
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 媒体卡片读的文件同步更新)。
|
||
* §NEW 缓存分目录:cover_current.jpg 落 covers/(与歌曲 song_* 分池)。 */
|
||
static unsigned long curjpg_hash(void)
|
||
{
|
||
char path[600];
|
||
const char *covdir = g_st.ctx.cache.covers_dir[0] ? g_st.ctx.cache.covers_dir
|
||
: g_st.cache_dir;
|
||
snprintf(path, sizeof path, "%s/cover_current.jpg", covdir);
|
||
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;
|
||
}
|