fix(ncm): 三连修 — 封面切歌不更新/缓冲无反馈失败无声/暂停切歌边界 (REPORT §18)
- cover worker 忙等死循环(成功后不清目标,每圈解码+换入+gen++,CPU 100%+每帧整盘重绘) → 成功清目标转怠速; 失败指数退避 0.5-4s 连 8 次放弃+WARN 日志; 去重加在拉目标; cover_get 持锁+cover_put 修绘制期 UAF; g_host_mock 防真网守卫 - 封面兜底污染修复: 不再把单曲 al_pic 写进 g_st.cover_url(否则 al_pic 缺失曲回落成 上一首封面且撞去重 → 封面永停); plr_page_enter 强制重估自愈; pl_page_enter 回填歌单封面 - 缓冲 UI: BUFFER 态「缓冲中… N%」(g_play.dl_done/total, 下载回调累加); 播放错误中文 错误条+snack(旧版 err 无 UI 消费者=无声死等); STOPPED 态播放键=重试; play/next 清 err - 写失败 set_err 修连环跳歌(旧版静默 break 被外层当自然播完,流死时跳完歌单+狂发 song_url); still_current 护栏修下载途中切歌的状态/快照过期覆盖 - 验证(0 真实请求): cover_chain_sim 7 组+player_flow_sim P1-P5 全 PASS, 旧代码对照 T2/P2/P4 必败坐实根因; audiod_chain_sim 14/14; pagination/window/core+qemu 回归 PASS; 宿主+mipsel 自有代码 0 warning; 9 页离屏渲染 rc=0 (含新 mock 页 playerbuf/playererr) - ncm md5 50d41bbf4a379e925699ba013b997ffd (未部署真机, 留主会话)
This commit is contained in:
1 parent
614694ad4a
commit
6200a41b71
13 files changed
+820
-20
No files matched your search
@@ -0,0 +1,237 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
static int g_log_warns;
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...)
|
||||
{
|
||||
if (lvl >= MEOW_LOG_WARN) g_log_warns++;
|
||||
(void)fmt;
|
||||
}
|
||||
|
||||
/* ---- 假 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;
|
||||
}
|
||||
|
||||
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 唯一);可注入失败/延迟 ---- */
|
||||
static int g_http_calls;
|
||||
static int g_fail_next; /* 剩余注入失败次数 */
|
||||
static int g_delay_ms; /* 模拟慢网络 */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* URL 期望内容哈希(与 stub 生成式一致) */
|
||||
static unsigned long url_hash(const char *url)
|
||||
{
|
||||
unsigned char tmp[4096];
|
||||
unsigned long h = djb2(url);
|
||||
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"
|
||||
|
||||
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("== T7: null/empty + reset ==\n");
|
||||
cover_request(NULL);
|
||||
cover_request("");
|
||||
CHECK(g_http_calls == before + 8, "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;
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/* bench/player_flow_sim.c — 播放线程状态机离线回归(0 真实请求,REPORT §18)。
|
||||
*
|
||||
* 驱动真实 app/play/player_manager.c(外层 player_thread + run_song 全链路),
|
||||
* 解码器/音频输出/网络/缓存全部本地 stub(可注入:写失败、url 失败、慢下载)。
|
||||
*
|
||||
* 用例:
|
||||
* P1 自然播完 → 自动顺延下一首(曲终不停在原地,err=0);
|
||||
* P2 音频写失败(流死/audiod 断链)→ **置错停播,cur_song 不动**——§18 前静默 break
|
||||
* 被外层当「自然播完」连环跳歌+狂发 song_url(本用例在旧代码上 FAIL);
|
||||
* P3 song_url 无音源 → STOPPED + NCM_ERR_NOTFOUND + errmsg(UI 有错误可显示);
|
||||
* P4 下载途中切歌 → 旧曲下载完成后**不得**把状态改回 PLAYING/写过期快照
|
||||
* (still_current 护栏;旧代码在切歌后 ~下载完成点 会把 BUFFER 覆盖成 PLAYING);
|
||||
* P5 错误后 play(cur) 重试 → 状态重新走 BUFFER→PLAYING,err 清零。
|
||||
*
|
||||
* 编译(宿主,ncm_app 目录下):
|
||||
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
* -Icore -Ivendor/jsmn -Ivendor/minimp3 -c bench/player_flow_sim.c -o /tmp/pf.o
|
||||
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
* -Icore -Ivendor/jsmn -Ivendor/minimp3 -c app/play/player_manager.c -o /tmp/pf_pm.o
|
||||
* gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o -lpthread -lm
|
||||
* 运行:./player_flow_sim (0 网络;PASS/FAIL 退出码)
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h"
|
||||
#include "minimp3_ex.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <unistd.h>
|
||||
|
||||
ncm_app_state_t g_st;
|
||||
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
|
||||
volatile int g_app_quit = 0;
|
||||
volatile int g_net_ready = 1;
|
||||
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;
|
||||
}
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (void)ctx; }
|
||||
|
||||
/* ---- appstate stub:窗内全命中,id=1000+idx ---- */
|
||||
int appstate_song_copy(int gidx, ncm_song_t *out)
|
||||
{
|
||||
if (gidx < 0 || gidx >= g_st.track_ids_count) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
snprintf(out->id, sizeof out->id, "%d", 1000 + gidx);
|
||||
snprintf(out->name, sizeof out->name, "Song%d", gidx);
|
||||
out->duration_ms = 180000;
|
||||
return 0;
|
||||
}
|
||||
int appstate_song_fetch(int gidx, ncm_song_t *out) { return appstate_song_copy(gidx, out) == 0 ? NCM_OK : NCM_ERR_ARG; }
|
||||
|
||||
/* ---- 缓存/网络 stub ----
|
||||
* g_cache_ok=1 → lookup 命中(给真实存在的 /tmp 文件,免下载);
|
||||
* 否则走下载:song_url(g_url_rc 可注入失败) → http_download(g_dl_delay_ms 模拟慢) → commit。 */
|
||||
static int g_cache_ok = 1;
|
||||
static int g_url_rc = NCM_OK;
|
||||
static int g_dl_delay_ms = 0;
|
||||
static int g_downloads; /* http_download 调用数(song_url 请求放大检测) */
|
||||
|
||||
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz)
|
||||
{
|
||||
(void)c; (void)id; (void)ext;
|
||||
if (!g_cache_ok) return NCM_ERR_NOENT;
|
||||
snprintf(out, out_sz, "/tmp/pf_sim.mp3");
|
||||
return NCM_OK;
|
||||
}
|
||||
int ncm_cache_open(const ncm_cache_t *c, const char *id, const char *ext, ncm_cache_writer_t **w)
|
||||
{ (void)c; (void)id; (void)ext; *w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK; }
|
||||
int ncm_cache_write(ncm_cache_writer_t *w, const unsigned char *d, size_t l) { (void)w; (void)d; (void)l; return NCM_OK; }
|
||||
int ncm_cache_commit(ncm_cache_writer_t *w) { free(w); g_cache_ok = 1; return NCM_OK; }
|
||||
int ncm_cache_abort(ncm_cache_writer_t *w) { free(w); return NCM_OK; }
|
||||
int ncm_cache_evict(const ncm_cache_t *c) { (void)c; return NCM_OK; }
|
||||
void ncm_cache_set_limit_gb(ncm_cache_t *c, int gb) { (void)c; (void)gb; }
|
||||
|
||||
int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int level, ncm_play_url_t *out)
|
||||
{
|
||||
(void)dev; (void)ck; (void)id; (void)level;
|
||||
if (g_url_rc != NCM_OK) return g_url_rc;
|
||||
memset(out, 0, sizeof *out);
|
||||
snprintf(out->url, sizeof out->url, "http://stub/%s.mp3", id);
|
||||
out->size = 4 * 1024 * 1024;
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
|
||||
size_t maxb, ncm_http_resp_t *resp)
|
||||
{
|
||||
(void)req; (void)maxb;
|
||||
g_downloads++;
|
||||
if (g_dl_delay_ms > 0) usleep((useconds_t)g_dl_delay_ms * 1000);
|
||||
unsigned char chunk[65536];
|
||||
memset(chunk, 0x5A, sizeof chunk);
|
||||
for (int i = 0; i < 8; i++) /* 512KB 总分发(进度回调会走 8 次) */
|
||||
if (cb(ctx, chunk, sizeof chunk) != 0) break;
|
||||
memset(resp, 0, sizeof *resp);
|
||||
resp->status = 200;
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
/* ---- 解码器 stub:g_eof 前恒有数据(P1 用 g_reads_left 控制播完点) ---- */
|
||||
static int g_eof;
|
||||
static int g_reads_left = -1; /* >=0: 剩余读次数后 EOF;-1: 直到 g_eof */
|
||||
|
||||
int mp3dec_ex_open(mp3dec_ex_t *dec, const char *file_name, int flags)
|
||||
{ (void)dec; (void)file_name; (void)flags; return 0; }
|
||||
void mp3dec_ex_close(mp3dec_ex_t *dec) { (void)dec; }
|
||||
int mp3dec_ex_seek(mp3dec_ex_t *dec, uint64_t position) { (void)dec; (void)position; return 0; }
|
||||
size_t mp3dec_ex_read(mp3dec_ex_t *dec, mp3d_sample_t *buf, size_t samples)
|
||||
{
|
||||
(void)dec;
|
||||
if (g_eof) return 0;
|
||||
if (g_reads_left == 0) return 0;
|
||||
if (g_reads_left > 0) g_reads_left--;
|
||||
memset(buf, 0, samples * sizeof(mp3d_sample_t));
|
||||
return samples;
|
||||
}
|
||||
|
||||
/* ---- 音频输出 stub:g_write_fail 注入写失败 ---- */
|
||||
static int g_write_fail;
|
||||
static int g_writes;
|
||||
static int g_opens; /* audio_out_open 次数(每首 run_song 开一次) */
|
||||
|
||||
int audio_out_open(int rate, int ch, int bits) { (void)rate; (void)ch; (void)bits; g_opens++; return 0; }
|
||||
int audio_out_write(const short *s, int frames)
|
||||
{ (void)s; g_writes++; return g_write_fail ? -1 : frames; }
|
||||
int audio_out_pause(int paused) { (void)paused; return 0; }
|
||||
void audio_out_close(void) { }
|
||||
int audio_out_buffered_ms(void) { return 0; }
|
||||
void audio_out_set_target(int ms) { (void)ms; }
|
||||
|
||||
/* ---- 歌词(未被调用的 worker 会被 gc-sections 丢弃;lyric_clear 由 worker 用,防御性提供) ---- */
|
||||
void lyric_clear(void) { }
|
||||
int lyric_parse(const char *a, const char *b) { (void)a; (void)b; return 0; }
|
||||
|
||||
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)
|
||||
|
||||
static int state(void) { return player_manager_state(); }
|
||||
|
||||
static int wait_state(int want, int ms)
|
||||
{
|
||||
for (int t = 0; t < ms; t += 10) { if (state() == want) return 1; usleep(10 * 1000); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wait_err(int want, int ms) /* 等错误码出现(初始态可能已是 STOPPED,等状态有竞态) */
|
||||
{
|
||||
for (int t = 0; t < ms; t += 10) { if (player_manager_error() == want) return 1; usleep(10 * 1000); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wait_downloads(int want, int ms)
|
||||
{
|
||||
for (int t = 0; t < ms; t += 10) { if (g_downloads >= want) return 1; usleep(10 * 1000); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* 假 mp3 文件(cache 命中路径 stat 用) */
|
||||
FILE *f = fopen("/tmp/pf_sim.mp3", "wb");
|
||||
if (!f) { printf("FAIL tmp file\n"); return 1; }
|
||||
for (int i = 0; i < 4096; i++) fputc(i & 0xFF, f);
|
||||
fclose(f);
|
||||
|
||||
memset(&g_st, 0, sizeof g_st);
|
||||
g_st.track_ids_count = 3;
|
||||
for (int i = 0; i < 3; i++) g_st.song_ids[i] = 1000 + i;
|
||||
g_st.level = NCM_LEVEL_STANDARD;
|
||||
g_st.cache_limit_gb = 10;
|
||||
|
||||
if (player_manager_start() != 0) { printf("FAIL start\n"); return 1; }
|
||||
|
||||
printf("== P1: natural finish -> auto-advance, err=0 ==\n");
|
||||
g_cache_ok = 1; g_reads_left = -1; g_eof = 0; g_write_fail = 0;
|
||||
player_manager_play(0);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 2000), "song0 playing (持续读流态可捕获)");
|
||||
/* stub 解码/写瞬时完成,整列顺排在一次 poll 内跑完(cur 直接冲到 -1)——
|
||||
* 改用 audio_out_open 计数判定:3 首全开过 = 自然顺延链走通。 */
|
||||
g_reads_left = 1; /* song0 再读 1 次即 EOF → 自然播完 */
|
||||
for (int t = 0; t < 4000 && state() != NCM_PC_STOPPED; t += 20) {
|
||||
if (g_reads_left <= 0) g_reads_left = 1;
|
||||
usleep(20 * 1000);
|
||||
}
|
||||
CHECK(g_opens == 3, "auto-advance chain: audio opens=%d (expect 3 首全开)", g_opens);
|
||||
CHECK(state() == NCM_PC_STOPPED && player_manager_error() == 0,
|
||||
"end of playlist -> STOPPED err=%d (expect 0)", player_manager_error());
|
||||
|
||||
printf("== P2: write fail (stream dead) -> STOPPED + err, cur unchanged (不连环跳歌) ==\n");
|
||||
g_reads_left = -1; g_eof = 0; g_write_fail = 1; g_writes = 0;
|
||||
player_manager_play(1);
|
||||
CHECK(wait_err(NCM_ERR_IO, 2000), "err=NCM_ERR_IO after write fail (rc=%d)", player_manager_error());
|
||||
CHECK(wait_state(NCM_PC_STOPPED, 1000), "stopped after write fail");
|
||||
usleep(500 * 1000);
|
||||
CHECK(player_manager_playing_song() == 1, "cur=%d (expect 1, 未跳歌)", player_manager_playing_song());
|
||||
CHECK(state() == NCM_PC_STOPPED, "stays STOPPED (no cascade)");
|
||||
CHECK(g_writes <= 2, "writes=%d (≤2: 第一次失败即停,不连环)", g_writes);
|
||||
g_write_fail = 0;
|
||||
|
||||
printf("== P3: no stream url -> STOPPED + NOTFOUND + errmsg ==\n");
|
||||
g_url_rc = NCM_ERR_NOTFOUND; g_cache_ok = 0;
|
||||
player_manager_play(2);
|
||||
CHECK(wait_err(NCM_ERR_NOTFOUND, 2000), "err=NOTFOUND on no-stream (rc=%d)", player_manager_error());
|
||||
CHECK(wait_state(NCM_PC_STOPPED, 1000), "stopped on no-stream");
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
const char *em = g_play.errmsg;
|
||||
int has_msg = em[0] != 0;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
CHECK(has_msg, "errmsg set (UI 可显示)");
|
||||
g_url_rc = NCM_OK;
|
||||
|
||||
printf("== P4: switch during slow download -> no stale PLAYING overwrite ==\n");
|
||||
g_cache_ok = 0; g_dl_delay_ms = 250; g_eof = 0; g_reads_left = -1;
|
||||
player_manager_play(0);
|
||||
CHECK(wait_downloads(1, 3000), "song0 download started");
|
||||
player_manager_play(1); /* 旧曲下载途中切歌 */
|
||||
/* 旧曲下载 +250ms 完成;新代码 still_current 护栏直接返回(状态保持 BUFFER),
|
||||
* 旧代码会在此点把 state 覆盖成 PLAYING。+280ms 落在新曲下载窗口内(~+300 才开始)。 */
|
||||
usleep(280 * 1000);
|
||||
CHECK(state() == NCM_PC_BUFFER,
|
||||
"after switch+during new download state=%d (expect BUFFER=%d; 旧代码被旧曲改成 PLAYING)",
|
||||
state(), NCM_PC_BUFFER);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "new song eventually playing");
|
||||
ncm_song_t snap;
|
||||
CHECK(player_manager_current_song(&snap) == 0 && strcmp(snap.id, "1001") == 0,
|
||||
"snapshot id=%s (expect 1001, 无过期快照)", snap.id);
|
||||
g_eof = 1; /* 让新曲自然收尾 */
|
||||
usleep(200 * 1000);
|
||||
g_dl_delay_ms = 0;
|
||||
|
||||
printf("== P5: retry after error -> BUFFER->PLAYING, err cleared ==\n");
|
||||
g_eof = 0; g_reads_left = -1; g_url_rc = NCM_ERR_NOTFOUND;
|
||||
g_cache_ok = 0;
|
||||
player_manager_play(2); /* 制造错误 */
|
||||
CHECK(wait_err(NCM_ERR_NOTFOUND, 2000), "error state ready (rc=%d)", player_manager_error());
|
||||
g_url_rc = NCM_OK; /* 网络恢复 */
|
||||
player_manager_play(player_manager_playing_song()); /* 重试(UI 播放键路径) */
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "retry playing");
|
||||
CHECK(player_manager_error() == 0, "err cleared on retry");
|
||||
g_eof = 1;
|
||||
|
||||
usleep(200 * 1000);
|
||||
player_manager_stop_thread();
|
||||
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
|
||||
return g_fail;
|
||||
}
|
||||
Reference in new issue
Block a user