/* bench/search_sim.c — 搜索增强(REPORT §23)数据层离线回归(0 网络请求)。 * * 直接驱动 App 真实函数(app/logic/appstate.c):搜索历史(去重/≤12/最新在前)、 * 单曲/歌单两类独立结果集、翻页 offset 累加与 has_more、以及「搜索结果播放队列」 * 上下文(queue_src=SEARCH 读 search_songs[],不覆写歌单窗口)。 * meow_config_* 用内存 stub(历史持久化语义)。误触抑制(g_press_row+g_press_delta>10) * 属 UI 手势层,另在宿主 test_ncm_app.c 的 searchtap 自检中验证(真实页面+真实 SDK)。 * * 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -c bench/search_sim.c -o /tmp/ss.o * $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * -Icore -c app/logic/appstate.c -o /tmp/ss_app.o * $CC_HOST -O2 -Wl,--gc-sections -o search_sim /tmp/ss.o /tmp/ss_app.o -lpthread * 运行:./search_sim (0 网络;PASS/FAIL 退出码) */ #include "../app/ncm_app_internal.h" #include #include #include #include /* appstate.c 依赖的全局(app.c 提供;本 bench 只测搜索/历史路径) */ 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; size_t ncm_strlcpy(char *dst, const char *src, size_t sz); /* appstate_queue_song 的 PLAYLIST 分支引用 song_fetch→songmeta/ncm_api_song_detail; * 本 bench 只测 SEARCH 路径,这些分支不执行,提供 No-op 桩满足链接。 */ int songmeta_load(const char *cd, const char *id, ncm_song_t *out) { (void)cd;(void)id;(void)out; return NCM_ERR_IO; } int songmeta_save(const char *cd, const char *id, const ncm_song_t *s){ (void)cd;(void)id;(void)s; return NCM_ERR_IO; } int ncm_api_song_detail(const char *d, const char *c, const int64_t *ids, int n, ncm_song_t *out, int max, int *out_n) { (void)d;(void)c;(void)ids;(void)n;(void)out;(void)max; if (out_n) *out_n = 0; return NCM_ERR_IO; } /* meow_config 内存 stub:只存 ncm.search_history 单键,模拟持久化(历史读回)。 */ static char s_cfg[4096]; static int s_cfg_valid = 0; int meow_config_set(const char *key, const char *val) { (void)key; if (!val) return MEOW_CFG_ERR_INVAL; size_t n = strlen(val); if (n >= sizeof s_cfg) n = sizeof s_cfg - 1; memcpy(s_cfg, val, n); s_cfg[n] = 0; s_cfg_valid = 1; return MEOW_CFG_OK; } int meow_config_get(const char *key, char *out, int outsz) { (void)key; if (!s_cfg_valid || outsz <= 0) return MEOW_CFG_ERR_NOENT; size_t n = strlen(s_cfg); if (n >= (size_t)outsz) n = (size_t)outsz - 1; memcpy(out, s_cfg, n); out[n] = 0; return (s_cfg[0] || n) ? MEOW_CFG_OK : MEOW_CFG_ERR_NOENT; } 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_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) static ncm_song_t mk_song(int i) { ncm_song_t s; memset(&s, 0, sizeof s); snprintf(s.id, sizeof s.id, "%d", 100000 + i); snprintf(s.name, sizeof s.name, "Search Song %d", i); snprintf(s.art_name, sizeof s.art_name, "Artist %d", i); snprintf(s.album_name, sizeof s.album_name, "Album %d", i); s.duration_ms = 180000 + i * 1000; return s; } static ncm_playlist_t mk_pl(int i) { ncm_playlist_t p; memset(&p, 0, sizeof p); snprintf(p.id, sizeof p.id, "pl%d", 200000 + i); snprintf(p.name, sizeof p.name, "Playlist %d", i); p.track_count = 10 + i; return p; } int main(void) { /* ===== 搜索历史:去重 + ≤12 + 最新在前 ===== */ printf("[1] search history dedup/cap/newest-first\n"); appstate_search_history_clear(); appstate_search_history_add("alpha"); appstate_search_history_add("beta"); appstate_search_history_add("alpha"); /* 去重:alpha 前置 */ CHECK(appstate_search_history_count() == 2, "dedup keeps 2 (alpha, beta)"); char b1[64], b2[64]; appstate_search_history_get(0, b1, sizeof b1); appstate_search_history_get(1, b2, sizeof b2); CHECK(strcmp(b1, "alpha") == 0 && strcmp(b2, "beta") == 0, "newest first after re-add"); for (int i = 0; i < NCM_SEARCH_HIST_MAX + 5; i++) { /* 超 12 → 截断 */ char k[32]; snprintf(k, sizeof k, "kw%d", i); appstate_search_history_add(k); } CHECK(appstate_search_history_count() == NCM_SEARCH_HIST_MAX, "cap at %d", NCM_SEARCH_HIST_MAX); char top[64]; appstate_search_history_get(0, top, sizeof top); CHECK(strcmp(top, "kw16") == 0, "newest kw16 at front"); /* 15+2... kw16 为最后 add */ char mid[64]; appstate_search_history_get(8, mid, sizeof mid); CHECK(strcmp(mid, "kw8") == 0, "kw8 at index 8 (truncation preserves order)"); appstate_search_history_remove(0); CHECK(appstate_search_history_count() == NCM_SEARCH_HIST_MAX - 1, "remove index 0 shrinks"); char top2[64]; appstate_search_history_get(0, top2, sizeof top2); CHECK(strcmp(top2, "kw15") == 0, "remove shifts next to front"); /* 持久化读回(stub meow_config 已落盘) */ appstate_search_history_clear(); CHECK(appstate_search_history_count() == 0, "clear to 0"); /* ===== 单曲/歌单两类独立结果集 ===== */ printf("[2] independent song/playlist result sets\n"); g_st.queue_src = NCM_QUEUE_PLAYLIST; appstate_search_clear(0); appstate_search_clear(1); ncm_song_t s0[4]; for (int i = 0; i < 4; i++) s0[i] = mk_song(i); appstate_search_append_songs(4, s0, 100); CHECK(appstate_search_count(0) == 4, "song set count=4"); CHECK(appstate_search_count(1) == 0, "playlist set independent (=0)"); ncm_playlist_t p0[2]; for (int i = 0; i < 2; i++) p0[i] = mk_pl(i); appstate_search_append_pls(2, p0, 50); CHECK(appstate_search_count(1) == 2, "playlist set count=2"); CHECK(appstate_search_count(0) == 4, "song set unchanged after playlist append"); ncm_song_t cpy; CHECK(appstate_search_song(1, &cpy) == 0 && strcmp(cpy.name, "Search Song 1") == 0, "copy song #1"); ncm_playlist_t pcpy; CHECK(appstate_search_pl(0, &pcpy) == 0 && strcmp(pcpy.name, "Playlist 0") == 0, "copy playlist #0"); appstate_search_clear(0); CHECK(appstate_search_count(0) == 0 && appstate_search_count(1) == 2, "clear song set leaves playlist"); /* ===== 翻页 offset 累加 / has_more ===== */ printf("[3] pagination offset accumulation / has_more\n"); appstate_search_clear(0); CHECK(appstate_search_has_more(0) == 1 && appstate_search_offset(0) == 0, "clear resets offset=0 more=1"); ncm_song_t page[30]; for (int i = 0; i < NCM_SEARCH_PAGE; i++) page[i] = mk_song(100 + i); appstate_search_append_songs(NCM_SEARCH_PAGE, page, 300); /* page1: total 300 */ CHECK(appstate_search_count(0) == NCM_SEARCH_PAGE, "page1 count=30"); CHECK(appstate_search_offset(0) == NCM_SEARCH_PAGE, "offset=30 after page1"); CHECK(appstate_search_has_more(0) == 1, "more=1 (30<300)"); appstate_search_append_songs(NCM_SEARCH_PAGE, page, 300); /* page2 */ CHECK(appstate_search_count(0) == 2 * NCM_SEARCH_PAGE && appstate_search_offset(0) == 60, "offset=60 after page2"); ncm_song_t tail[30]; for (int i = 0; i < NCM_SEARCH_PAGE; i++) tail[i] = mk_song(200 + i); appstate_search_append_songs(NCM_SEARCH_PAGE, tail, 240); /* page3 → 90/240 */ CHECK(appstate_search_has_more(0) == 1 && appstate_search_offset(0) == 90, "more=1 at 90/240"); for (int i = 0; i < 5; i++) appstate_search_append_songs(30, tail, 240); /* +150 → 240/240 */ CHECK(appstate_search_count(0) == 240 && appstate_search_offset(0) == 240, "reached total 240"); CHECK(appstate_search_has_more(0) == 0, "more=0 at total"); /* 总量 > 数组上限:clamp 到 NCM_SEARCH_SONG_MAX,has_more 仍按服务端总数反映 */ appstate_search_clear(0); for (int i = 0; i < NCM_SEARCH_SONG_MAX / NCM_SEARCH_PAGE + 3; i++) appstate_search_append_songs(NCM_SEARCH_PAGE, page, 2000); CHECK(appstate_search_count(0) == NCM_SEARCH_SONG_MAX, "clamp at NCM_SEARCH_SONG_MAX"); CHECK(appstate_search_has_more(0) == 1, "more=1 while server total (2000) > cap"); /* 服务端 total 恰好等于 cap → 加满后 more=0 */ appstate_search_clear(0); for (int i = 0; i < NCM_SEARCH_SONG_MAX / NCM_SEARCH_PAGE + 1; i++) appstate_search_append_songs(NCM_SEARCH_PAGE, page, NCM_SEARCH_SONG_MAX); CHECK(appstate_search_count(0) == NCM_SEARCH_SONG_MAX && appstate_search_has_more(0) == 0, "more=0 when server total == cap (clamped full)"); /* 无 total(-1)时回落「返回条数==页大小」判 more */ appstate_search_clear(1); ncm_playlist_t pp[NCM_SEARCH_PAGE]; for (int i = 0; i < NCM_SEARCH_PAGE; i++) pp[i] = mk_pl(i); appstate_search_append_pls(NCM_SEARCH_PAGE, pp, -1); CHECK(appstate_search_has_more(1) == 1, "unknown total + full page => more=1"); appstate_search_append_pls(10, pp, -1); CHECK(appstate_search_has_more(1) == 0, "unknown total + short page => more=0"); /* ===== 搜索结果播放队列上下文(§23:不覆写歌单窗口) ===== */ printf("[4] search queue context (queue_src=SEARCH)\n"); appstate_search_clear(0); for (int i = 0; i < 3; i++) { ncm_song_t t = mk_song(300 + i); appstate_search_append_songs(1, &t, 3); } /* 歌单窗口预置(模拟「打开的歌单还没被打扰」) */ g_st.track_ids_count = 7; g_st.song_count = 7; g_st.window_base = 0; memset(g_st.song_valid, 0, sizeof g_st.song_valid); for (int i = 0; i < 7; i++) { g_st.song_ids[i] = 50 + i; g_st.song_valid[i] = 1; } appstate_set_queue(NCM_QUEUE_PLAYLIST); CHECK(appstate_queue_count() == 7, "queue=playlist window count(7)"); appstate_set_queue(NCM_QUEUE_SEARCH); CHECK(appstate_queue_count() == 3, "queue=search count(3)"); ncm_song_t qs; CHECK(appstate_queue_song(2, &qs) == 0 && strcmp(qs.name, "Search Song 302") == 0, "queue_song from search set"); /* 关键:搜索起播不改写歌单窗口(track_ids_count/song_ids 原样) */ CHECK(g_st.track_ids_count == 7 && g_st.song_valid[3] == 1 && g_st.song_ids[3] == 53, "playlist window untouched by search queue"); appstate_set_queue(NCM_QUEUE_PLAYLIST); CHECK(appstate_queue_count() == 7, "queue back to playlist count(7)"); printf("\n%s\n", g_fail ? "search_sim FAIL" : "search_sim ALL PASS"); return g_fail ? 1 : 0; }