/* bench/pagination_sim.c — 三期第一批 歌单分页 离线状态机验证(0 网络请求)。 * * 直接驱动 App 的真实函数 appstate_load_playlist_songs_page()(app/logic/appstate.c), * 仅把两个网络端点(ncm_api_playlist_detail / ncm_api_song_detail)替换成本地 stub * (合成 trackIds / 合成歌曲对象),验证 detail 后按页 song_detail 的累加/封顶/尾部语义: * ① offset=0 重建索引(song_ids[]←全量 trackIds,track_ids_count)并载第 0 页; * ② 逐页 offset 前进,song_count 单调累加; * ③ 尾部(song_count==track_ids_count)不再载下一页;④ NCM_SONG_MAX 封顶不越界。 * 风控纪律:本验证 0 次真实 NCM 请求(离线 stub)。歌曲上限 = NCM_SONG_MAX(内存封顶); * 本模拟用两种歌单规模:(a) 130 首(< 上限,读全程);(b) 5191 首(> 上限,读满 NCM_SONG_MAX 截断)。 * * 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -I../vendor/jsmn -c bench/pagination_sim.c -o /tmp/ps.o * $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -I../vendor/jsmn -c app/logic/appstate.c -o /tmp/ps_app.o * $CC_HOST -O2 -Wl,--gc-sections -o pagination_sim /tmp/ps.o /tmp/ps_app.o -lpthread * 运行:./pagination_sim (0 网络;PASS/FAIL 退出码) */ #include "../app/ncm_app_internal.h" #include #include #include /* appstate.c 依赖的全局(app.c 提供;本 bench 只测分页路径,仅 g_st 被用到) */ 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 = 0; /* ncm_strlcpy:appstate_apply_playlist_cover 用到(从 core ncm_common 借;此处最小内联实现) */ 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; } /* ---- 网络端点 stub(离线,合成数据) ---- */ static int g_pid_total; /* detail 应返回的 trackIds 总数(供测试注入) */ static int g_detail_calls; static int g_song_calls; int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id, char *name, size_t name_sz, char *cover, size_t cover_sz, int64_t *ids, int id_cap, int *out_id_n) { (void)device_id; (void)cookie; (void)cover_sz; g_detail_calls++; if (name) snprintf(name, name_sz, "Sim Playlist"); if (cover) cover[0] = 0; int n = (g_pid_total < id_cap) ? g_pid_total : id_cap; for (int i = 0; i < n; i++) ids[i] = 1000000LL + i; /* 合成连续 id */ *out_id_n = n; return NCM_OK; } int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n, ncm_song_t *out, int max, int *out_n) { (void)device_id; (void)cookie; g_song_calls++; int m = (n < max) ? n : max; for (int i = 0; i < m; i++) { snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)ids[i]); snprintf(out[i].name, sizeof out[i].name, "Song%lld", (long long)ids[i]); snprintf(out[i].art_name, sizeof out[i].art_name, "Artist%lld", (long long)ids[i]); snprintf(out[i].al_pic, sizeof out[i].al_pic, "http://p.music.126.net/cov%lld.jpg", (long long)ids[i]); } *out_n = m; return NCM_OK; } /* ---- 驱动:整批载入到尾部/上限,返回最终 song_count ---- */ static int load_all(int pid_total, int *detail_calls, int *song_calls) { memset(&g_st, 0, sizeof g_st); g_st.playlist_count = 1; snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl0"); g_st.playlists[0].track_count = pid_total; g_pid_total = pid_total; g_detail_calls = 0; g_song_calls = 0; int rc = appstate_load_playlist_songs_page(0, 0, NCM_SONG_BATCH); if (rc != NCM_OK) { printf(" page0 rc=%d\n", rc); return -1; } int off = g_st.song_count; while (off < g_st.track_ids_count && off < NCM_SONG_MAX) { rc = appstate_load_playlist_songs_page(0, off, NCM_SONG_BATCH); if (rc != NCM_OK) { printf(" page(%d) rc=%d\n", off, rc); return -1; } int nxt = g_st.song_count; if (nxt <= off) break; off = nxt; } if (detail_calls) *detail_calls = g_detail_calls; if (song_calls) *song_calls = g_song_calls; return g_st.song_count; } static int g_fail = 0; #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) int main(void) { /* (a) 130 首(< NCM_SONG_MAX):应读全程,track_ids_count=130,song_count=130,尾部停。 */ printf("== case A: 130 songs (reads to tail) ==\n"); int dc = 0, sc = 0; int n = load_all(130, &dc, &sc); CHECK(n == 130, "song_count=%d (expect 130)", n); CHECK(g_st.track_ids_count == 130, "track_ids_count=%d (expect 130)", g_st.track_ids_count); CHECK(dc == 1, "detail calls=%d (expect 1, index fetched once)", dc); CHECK(sc == 2, "song_detail calls=%d (expect 2 = 100 + 30 pages)", sc); CHECK(strcmp(g_st.songs[129].name, "Song1000129") == 0, "last song name='%s'", g_st.songs[129].name); CHECK(g_st.songs[129].al_pic[0] != 0, "last song has al_pic='%s'", g_st.songs[129].al_pic); /* 越过尾部再 load:应无操作(不越界) */ int rc = appstate_load_playlist_songs_page(0, 130, NCM_SONG_BATCH); CHECK(rc == NCM_OK && g_st.song_count == 130, "load past tail: rc=%d song_count=%d", rc, g_st.song_count); /* (b) 5191 首(> NCM_SONG_MAX):索引封顶 NCM_SONG_MAX,读满封顶截断。 */ printf("== case B: 5191 songs (capped at NCM_SONG_MAX) ==\n"); dc = sc = 0; n = load_all(5191, &dc, &sc); CHECK(n == NCM_SONG_MAX, "song_count=%d (expect NCM_SONG_MAX=%d)", n, NCM_SONG_MAX); CHECK(g_st.track_ids_count == NCM_SONG_MAX, "track_ids_count=%d (index capped)", g_st.track_ids_count); CHECK(dc == 1, "detail calls=%d", dc); CHECK(sc == NCM_SONG_MAX / NCM_SONG_BATCH, "song_detail calls=%d (expect %d full pages)", sc, NCM_SONG_MAX / NCM_SONG_BATCH); /* 越界再 load:应 NCM_OK 但不动 song_count */ rc = appstate_load_playlist_songs_page(0, NCM_SONG_MAX, NCM_SONG_BATCH); CHECK(rc == NCM_OK && g_st.song_count == NCM_SONG_MAX, "load past cap: rc=%d song_count=%d", rc, g_st.song_count); /* (c) 空歌单(0 首) */ printf("== case C: empty playlist ==\n"); n = load_all(0, &dc, &sc); CHECK(n == 0 && g_st.track_ids_count == 0, "empty: song_count=%d track_ids_count=%d", n, g_st.track_ids_count); printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS"); return g_fail ? 1 : 0; }