/* bench/lyric_sim.c — 歌词「掉线失败 → 重连 → 重试成功」宿主离线回归(0 真实请求)。 * * 驱动真实 app/play/player_manager.c 的 lyric_worker(后台歌词线程),网络换成可注入的 * ncm_api_lyric 桩:第 1 次调用返回 NCM_ERR_NET(模拟掉线瞬间歌词请求失败),第 2 次返回 * NCM_OK + 一段 LRC(模拟网络恢复后重试成功)。 * * 复现的真机症状(REPORT/任务):播放中歌词正常;网络掉线一会儿再重连后某曲恒「暂无歌词」。 * 根因:旧 lyric_worker 在**发起网络请求前**就把 last_id 记为已拉,且失败也置 lyric_state=2 * (=已拉完),故网络失败被写死成「无歌词」,重连后永不重试。本机回归: * ① 第 1 次失败后 lyric_state() 必须**不是** 2(可重试),且无词(lyric_count()==0); * ② 推进单调时钟越过退避点(模拟重连)后必须**自动重试**并成功(第 2 次调用)+ 词展示 * (lyric_count()>0、lyric_state()==2)。 * 旧代码在 ② 上 FAIL(g_lyr_state 恒 2、第 2 次调用永不发生)。 * * 编译(宿主,ncm_app 目录下;gc-sections 只保留歌词路径,避免链整个播放链的符号): * CC=gcc F="-ffunction-sections -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd -Ivendor/jsmn" * $CC $F -c bench/lyric_sim.c -o /tmp/ly.o * $CC $F -c app/play/player_manager.c -o /tmp/ly_pm.o * $CC $F -c app/logic/lyric.c -o /tmp/ly_lrc.o * $CC -Wl,--gc-sections -o lyric_sim /tmp/ly.o /tmp/ly_pm.o /tmp/ly_lrc.o -lpthread -lm * 运行:./lyric_sim (PASS/FAIL 退出码) */ #include "../app/ncm_app_internal.h" #include #include #include #include /* usleep */ /* ---- 全局状态(app.c 定义;本 sim 提供桩) ---- */ 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 = 0; int g_host_mock = 1; int g_dark = 0; /* ---- 可控单调时钟(lyric_worker 用 ncm_monotonic_ms 做失败退避) ---- */ static int64_t g_now_ms = 1000; int64_t ncm_monotonic_ms(void) { return g_now_ms; } 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; } /* appstate 桩:单曲队列(idx 0 恒有效,song id=1000) */ int appstate_queue_count(void) { return 1; } /* page-cache 治理桩:恒非暂停(不挡歌词预取) */ int meow_cachegov_prefetch_paused(void) { return 0; } /* 媒体会话歌词上报桩:失败上报空、成功上报 LRC;计数供断言 */ static int g_report_empty; static int g_report_lyric; static char g_report_last_lrc[256]; void ipc_media_lyric_report(const char *lrc, const char *tl) { (void)tl; if (lrc && lrc[0]) { g_report_lyric++; ncm_strlcpy(g_report_last_lrc, lrc, sizeof g_report_last_lrc); } else { g_report_empty++; } } /* 歌词 API 桩:第 1 次=网络失败,其后=成功返回 LRC。 */ static int g_fetch_calls; int ncm_api_lyric(const char *device_id, const char *cookie, const char *id, char *lrc, size_t lrc_sz, char *tlrc, size_t tlrc_sz) { (void)device_id; (void)cookie; (void)id; (void)tlrc; (void)tlrc_sz; g_fetch_calls++; if (g_fetch_calls == 1) { if (lrc && lrc_sz) lrc[0] = 0; /* 掉线失败:不写词 */ return NCM_ERR_NET; /* 网络错误(掉线) */ } static const char body[] = "[00:00.00]Line one\n[00:05.00]Line two\n[00:10.00]Line three\n"; if (lrc && lrc_sz) snprintf(lrc, lrc_sz, "%s", body); return NCM_OK; } static void wait_ms(int ms) { for (int i = 0; i < ms; i += 20) { usleep(20000); if (g_app_quit) break; } } static int wait_until(const volatile int *val, int target, int timeout_ms) { for (int i = 0; i < timeout_ms; i += 10) { if (*val >= target) return 1; usleep(10000); } return (*val >= target); } int main(void) { memset(&g_st, 0, sizeof g_st); pthread_mutex_init(&g_play.mu, NULL); g_play.state = NCM_PC_PLAYING; g_play.cur_song = 0; g_play.play_song_valid = 1; ncm_strlcpy(g_play.play_song.id, "1000", sizeof g_play.play_song.id); if (player_manager_load_lyric() != NCM_OK) { printf("FAIL start worker\n"); return 1; } int ok = 1; /* ① 第 1 次拉取失败(网络错误):须可重试(state!=2)、无词 */ if (!wait_until(&g_fetch_calls, 1, 3000)) { printf("FAIL first fetch never happened\n"); return 1; } wait_ms(400); /* 让 worker 完成失败收尾(state=0, retry_at 已算) */ int st1 = lyric_state(); int n1 = lyric_count(); int ok1 = (g_fetch_calls == 1 && st1 != 2 && n1 == 0); printf("step1 failure: calls=%d state=%d (expect !=2) count=%d rep_empty=%d\n", g_fetch_calls, st1, n1, g_report_empty); printf(" -> %s\n", ok1 ? "PASS" : "FAIL"); ok &= ok1; /* 模拟网络重连:推进单调时钟越过失败退避点 */ g_now_ms = 10000; /* ② 重试自动发生并成功:第 2 次调用、state=2、词已展示 */ if (!wait_until(&g_fetch_calls, 2, 4000)) { printf("FAIL retry never fired (calls=%d) — old code keeps state=2, no retry\n", g_fetch_calls); return 1; } int st2 = lyric_state(); int n2 = lyric_count(); int ok2 = (g_fetch_calls == 2 && st2 == 2 && n2 > 0 && g_report_lyric >= 1); printf("step2 retry: calls=%d state=%d count=%d rep_lyric=%d lrc=%s\n", g_fetch_calls, st2, n2, g_report_lyric, g_report_last_lrc); printf(" -> %s\n", ok2 ? "PASS" : "FAIL"); ok &= ok2; g_app_quit = 1; printf("lyric_sim overall -> %s\n", ok ? "PASS" : "FAIL"); fflush(stdout); return ok ? 0 : 1; }