feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)

脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
This commit is contained in:
AskaEth committed 2026-11-04 20:02:28 +08:00
1 parent e87ccdf061
commit 7c7d891570
59 files changed
+5305 -590

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
/* 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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h> /* 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;
}
+277
View File
@@ -0,0 +1,277 @@
/* bench/plpage_sim.c — 歌单页「懒加载分页 + 加载后列表刷新」离线回归(0 真实 NCM 请求)。
*
* 驱动真实 app/ui/playlist.c(页面打开 / 滚动触发 / worker / tick 消费全链路)+ 真实
* appstate.c(滑动窗口)+ 真实 songmeta.c(/tmp 盘缓存)+ 宿主 SDK(meow_listview/draw 全真,
* 与 ncm_host 同一批 out/host/sdk_*.o)。网络端点(ncm_api_playlist_detail /
* ncm_api_song_detail)为本地合成 stub(计调用次数、可注入单批失败)。
*
* Bug1(大歌单懒加载分页)验证点:
* ① 打开 400 首歌单:首屏 = 1 detail + 恰 1 批 song_detail(物化 [0,100),第 100 首未物化)
* ——不再首屏连拉多批补满窗口(旧行为 ≤3 批连发,5k+ 歌单阻塞 + 风控);
* ② 顶部小幅滚动(视口底距已分页底部 >20 首):不发任何新请求;
* ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败时 g_paged_hi 不推进(占位保留),
* 再滑到底部重试同一页成功(一次一页,不连拉);
* ④ 逐页到底(400 = 4 页);全部拉完后滑到尾部:不再发请求(停止);
* ⑤ 回滚到顶部:失效槽(窗口挤出)由盘缓存补物化,song_detail 调用数 0 增长(不占网络额度);
* Bug2(加载完列表刷新)验证点:
* ⑥ 首屏/每页 worker 完成经 playlist_tick 消费后,整帧重画计数(--wrap=meow_ui_paint_full)
* 递增,且新物化行经 appstate_song_copy 取到真实歌名/歌手(= pl_src2 将绑定真实数据,
* 不再是「... 加载中」占位)。
* 风控纪律:0 次真实 NCM 请求(离线 stub + /tmp 盘缓存)。
*
* 编译/运行见 build_ncm_sims.sh「plpage_sim」段(宿主,ncm_app 目录下)。
*/
#include "../app/ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.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 = 0;
int g_host_mock = 0; /* 真实分页链路(不走 mock 跳网分支) */
/* ---- core 符号最小实现(同 playlist_race_sim 口径) ---- */
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;
}
int64_t ncm_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* ---- cover / 播放 stub(本 sim 不测封面与播放;playlist.c 引用,链接需要) ---- */
void cover_request(const char *url, const char *song_id) { (void)url; (void)song_id; }
meow_img *cover_get(void) { return NULL; }
void cover_put(void) {}
int cover_changed(void) { return 0; }
void cover_ack(void) {}
void cover_cache_reset(void) {}
void player_play_song(int idx) { (void)idx; }
/* ---- core 符号(appstate.c 引用;运行时不到达,仅链接需要) ---- */
int ncm_login_status_check(const char *device_id, const char *cookie, char *uid, size_t uid_sz,
char *nick, size_t nick_sz)
{ (void)device_id;(void)cookie;(void)uid;(void)uid_sz;(void)nick;(void)nick_sz; return NCM_ERR_AUTH; }
int ncm_ctx_clear_cookie(ncm_ctx_t *ctx) { (void)ctx; return NCM_OK; }
int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid,
int limit, int offset, ncm_playlist_t *out, int max, int *out_n)
{ (void)device_id;(void)cookie;(void)uid;(void)limit;(void)offset;(void)out;(void)max;(void)out_n; return NCM_ERR_NET; }
/* ---- 网络端点 stub(离线,合成数据;计调用次数 + 可注入单批失败) ---- */
#define SIM_TOTAL 400 /* 4 页:100 × 4(> NCM_SONG_WINDOW 256,回滚必滑窗) */
static int g_detail_calls;
static int g_song_calls; /* song_detail 成功批次数 */
static int g_song_attempts; /* song_detail 发起次数(含失败批) */
static int g_song_fail_n; /* 接下来 N 批 song_detail 网络失败模拟 */
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)playlist_id; (void)cover_sz;
g_detail_calls++;
if (name) snprintf(name, name_sz, "Sim Playlist");
if (cover) cover[0] = 0;
int n = (SIM_TOTAL < id_cap) ? SIM_TOTAL : id_cap;
for (int i = 0; i < n; i++) ids[i] = 1000000LL + i;
*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_attempts++;
if (g_song_fail_n > 0) { /* 单批网络失败模拟(失败不推进分页高水位 → 同页重试) */
g_song_fail_n--;
if (out_n) *out_n = 0;
return NCM_ERR_NET;
}
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].album_name, sizeof out[i].album_name, "Album%lld", (long long)ids[i]);
out[i].duration_ms = 200000;
out[i].fee = 0;
}
if (out_n) *out_n = m;
return NCM_OK;
}
/* ---- Bug2 证据:整帧重画计数(链接时 --wrap=meow_ui_paint_full 重定向到此) ---- */
extern void __real_meow_ui_paint_full(void);
static int g_paint_full_calls;
void __wrap_meow_ui_paint_full(void)
{
g_paint_full_calls++;
__real_meow_ui_paint_full();
}
/* ---- 驱动辅助:模拟 app_on_tick 周期性消费 worker 完成 ---- */
static int pump_until(int (*cond)(void), int timeout_ms)
{
for (int t = 0; t < timeout_ms; t += 5) {
playlist_tick(0);
if (cond()) {
/* 再喂若干 tick:确保完成态被消费(g_plthr_on 清零),后续滚动可再触发 */
for (int k = 0; k < 30; k++) { playlist_tick(0); usleep(2000); }
return 1;
}
usleep(5000);
}
return 0;
}
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)
/* 条件(C 无闭包,用全局目标值) */
static int g_want_calls;
static int cond_calls(void) { return g_song_calls >= g_want_calls; }
static int g_want_attempts;
static int cond_attempts(void) { return g_song_attempts >= g_want_attempts; }
static int cond_first_screen(void)
{
return g_st.track_ids_count == SIM_TOTAL && g_song_calls >= 1;
}
/* 滚动到指定首行(行高 APP_ROW_H px;swipe 距离按当前行差换算) */
static void scroll_to_row(int row)
{
static int cur;
int d = row - cur;
cur = row;
playlist_swipe(d >= 0 ? MEOW_SWIPE_UP : MEOW_SWIPE_DOWN,
(d >= 0 ? d : -d) * APP_ROW_H);
}
int main(void)
{
printf("== plpage_sim: 懒加载分页 + 加载后列表刷新(%d 首,窗口 %d,批 %d,触底余量 20)==\n",
SIM_TOTAL, NCM_SONG_WINDOW, NCM_SONG_BATCH);
/* 宿主 SDK 离屏初始化(与 ncm_host 同口径) */
setenv("MEOW_UI_DUMP", "/tmp/plpage_sim.ppm", 0);
meow_ui_opts o = { APP_W, APP_H };
if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; }
meow_theme_set_mode(MEOW_THEME_DARK);
ncm_on_theme(MEOW_THEME_DARK);
/* 歌单 mock:1 张 SIM_TOTAL 首歌单;songmeta 盘缓存用临时目录 */
char cdt[] = "/tmp/plpage_cache_XXXXXX";
if (!mkdtemp(cdt)) { fprintf(stderr, "mkdtemp failed\n"); return 1; }
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "%s", cdt);
g_st.playlist_count = 1;
snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl0");
snprintf(g_st.playlists[0].name, sizeof g_st.playlists[0].name, "SimPlaylist");
g_st.playlists[0].track_count = SIM_TOTAL;
ncm_song_t sq;
int paint0;
/* ---- ① Bug1:打开歌单 → 首屏恰 1 detail + 1 批 song_detail(物化 [0,100)) ---- */
printf("== case 1: 首屏只拉一页 ==\n");
paint0 = g_paint_full_calls;
playlist_view_create();
playlist_open(0);
CHECK(pump_until(cond_first_screen, 8000), "first-screen loaded (track_ids=%d calls=%d)",
g_st.track_ids_count, g_song_calls);
CHECK(g_detail_calls == 1, "detail calls=%d (expect 1)", g_detail_calls);
CHECK(g_song_calls == 1, "song_detail calls=%d (expect 1, 不再首屏连拉多批)", g_song_calls);
CHECK(g_st.track_ids_count == SIM_TOTAL, "track_ids_count=%d (expect %d)",
g_st.track_ids_count, SIM_TOTAL);
CHECK(appstate_song_copy(99, &sq) == 0 && strcmp(sq.name, "Song1000099") == 0,
"row 99 materialized name='%s'", sq.name);
CHECK(appstate_song_copy(100, &sq) != 0, "row 100 NOT materialized yet (placeholder)");
/* Bug2:首屏完成 → 重画计数递增 + 行绑定真实歌名/歌手(非占位) */
CHECK(g_paint_full_calls > paint0, "paint_full after first-screen %d -> %d (list repainted)",
paint0, g_paint_full_calls);
CHECK(appstate_song_copy(5, &sq) == 0 && strcmp(sq.art_name, "Artist1000005") == 0,
"row 5 binds real artist='%s' (src2 填真实数据,非「... 加载中」)", sq.art_name);
/* ---- ② 顶部小幅滚动:不触发任何新请求(距已分页底部 >20 首) ---- */
printf("== case 2: 顶部滚动不拉页 ==\n");
scroll_to_row(10);
int a0 = g_song_attempts;
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
CHECK(g_song_attempts == a0, "song_detail attempts=%d (unchanged, 顶部滚动不拉页)", g_song_attempts);
/* ---- ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败不推进,再滑重试同页成功 ---- */
printf("== case 3: 触底拉下一页(一次一页;失败重试同页) ==\n");
g_song_fail_n = 1; /* 注入:下一次 song_detail 网络失败 */
int att_before = g_song_attempts;
scroll_to_row(90); /* 视口底 ≈97 行,余量 ≤20 → 触发第 2 页(失败) */
g_want_attempts = att_before + 1;
CHECK(pump_until(cond_attempts, 8000), "page-2 first attempt fired (attempts=%d)", g_song_attempts);
CHECK(g_song_calls == 1, "page-2 failed -> calls=%d (g_paged_hi 不推进,占位保留)", g_song_calls);
CHECK(appstate_song_copy(199, &sq) != 0, "row 199 still placeholder after failed page");
scroll_to_row(91); /* 再滑(滚动事件)→ 重试同一页 */
g_want_calls = 2;
CHECK(pump_until(cond_calls, 8000), "page 2 retried & loaded (calls=%d attempts=%d)",
g_song_calls, g_song_attempts);
CHECK(g_song_attempts == att_before + 2,
"attempts +%d (1 失败 + 1 重试同页 = 一次一页,未连拉)", g_song_attempts - att_before);
CHECK(appstate_song_copy(199, &sq) == 0 && strcmp(sq.name, "Song1000199") == 0,
"row 199 materialized name='%s'", sq.name);
CHECK(g_paint_full_calls > paint0, "paint_full after page-2 consumed (=%d)", g_paint_full_calls);
/* ---- ④ 逐页到底;全部拉完后滑到尾部:不再发请求 ---- */
printf("== case 4: 逐页到底 + 拉完即止 ==\n");
att_before = g_song_attempts;
scroll_to_row(190); /* 余量 ≤20 → 触发第 3 页 */
g_want_calls = 3;
CHECK(pump_until(cond_calls, 8000), "page 3 loaded (calls=%d)", g_song_calls);
scroll_to_row(290); /* 余量 ≤20 → 触发第 4 页(末页) */
g_want_calls = 4;
CHECK(pump_until(cond_calls, 8000), "page 4 (last) loaded (calls=%d)", g_song_calls);
CHECK(g_song_attempts == att_before + 2, "attempts +%d (第 3、4 页恰各 1 批)",
g_song_attempts - att_before);
CHECK(appstate_song_copy(399, &sq) == 0 && strcmp(sq.name, "Song1000399") == 0,
"tail row 399 materialized name='%s'", sq.name);
att_before = g_song_attempts;
scroll_to_row(390); /* 已在尾部:全部拉完 → 不再发请求 */
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
CHECK(g_song_attempts == att_before, "all loaded -> scroll at tail: attempts=%d (unchanged)",
g_song_attempts);
/* ---- ⑤ 回滚到顶部:失效槽(窗口挤出)盘缓存补物化,song_detail 0 增长 ---- */
printf("== case 5: 回滚补物化走盘缓存(0 网络) ==\n");
att_before = g_song_attempts;
scroll_to_row(0); /* 回顶部:窗口已滑走,行 0-35 失效 → 盘缓存补 */
for (int k = 0; k < 400 && !appstate_range_loaded(0, 30); k++) { playlist_tick(0); usleep(5000); }
CHECK(appstate_range_loaded(0, 30), "rows 0-29 re-materialized after scroll-back");
CHECK(g_song_attempts == att_before, "scroll-back refill: song_detail attempts=%d "
"(unchanged, 盘缓存命中不占网络)", g_song_attempts);
CHECK(appstate_song_copy(0, &sq) == 0 && strcmp(sq.name, "Song1000000") == 0,
"row 0 re-materialized name='%s'", sq.name);
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
/* 清理临时盘缓存(≤400 个小文件) */
{
char cmd[600];
snprintf(cmd, sizeof cmd, "rm -rf %s", cdt);
if (system(cmd) != 0) fprintf(stderr, "cleanup failed: %s\n", cdt);
}
meow_ui_shutdown();
return g_fail ? 1 : 0;
}
+45 -2
View File
@@ -40,8 +40,11 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
}
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接 */
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return -1; }
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接。
* 会话建立→音量同步用例(§切歌音量骤增回归)需要 audio_out_open 真正「开新流/分配新 sid」,
* 故 meow_audio_open 桩改为返回递增 sid(模拟 audiod 单调分配),供 §切歌 会话切换复现。 */
static int g_sim_sid = 0;
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return ++g_sim_sid; }
int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; }
int meow_audio_pause(int sid, int paused) { (void)sid;(void)paused; return 0; }
int meow_audio_close(int sid) { (void)sid; return 0; }
@@ -161,6 +164,46 @@ int main(void)
printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n");
}
/* §切歌音量骤增回归:会话建立→音量同步。切歌 = 新 audio_out_open = 新 audiod 流(新 sid),
* 必须**每会话重发一次**当前音量——此前 g_vol_synced 进程级「首同步一次」,新会话被跳过,
* DAC 停在会话建立后的默认响度(切歌骤增,按音量键才回落)。用真实 audio_out_open/close
* (meow_audio_open 桩返回递增 sid)断言:新会话必 +1 连接(重发音量),同会话内重复 sync 不新增。 */
{
int base = g_nconn; /* 上面值变重发测试后基线 = 2 条连接 */
/* 会话 1(首开):应重发 1 条 VOLUME,pct==当前 settings(50) */
if (audio_out_open(44100, 2, 16) != 0) { printf("FAIL session1 audio_out_open\n"); fail = 1; }
usleep(50000);
if (g_nconn != base + 1) {
printf("FAIL session1 establish: conns=%d (expect %d) — 新会话未重发音量\n", (int)g_nconn, base + 1);
fail = 1;
} else if (g_got_pct != 50) {
printf("FAIL session1 establish: pct=%d (expect 50)\n", (int)g_got_pct); fail = 1;
} else {
printf("PASS session1 establish -> 1 volume sync (pct=50)\n");
}
/* 同会话内反复 sync(同值):不得新建连接(防 fd 风暴) */
for (int i = 0; i < 20; i++) audio_out_sync_volume();
usleep(50000);
if (g_nconn != base + 1) {
printf("FAIL same-session repeat sync: conns=%d (expect %d)\n", (int)g_nconn, base + 1);
fail = 1;
} else {
printf("PASS same-session repeat sync -> no new connection (storm-free)\n");
}
audio_out_close();
/* 会话 2(切歌):再开 → 必须再重发 1 条(新 sid),pct==50 */
if (audio_out_open(44100, 2, 16) != 0) { printf("FAIL session2 audio_out_open\n"); fail = 1; }
usleep(50000);
if (g_nconn != base + 2) {
printf("FAIL session2 (song switch): conns=%d (expect %d) — 切歌未重发音量\n", (int)g_nconn, base + 2);
fail = 1;
} else if (g_got_pct != 50) {
printf("FAIL session2 switch: pct=%d (expect 50)\n", (int)g_got_pct); fail = 1;
} else {
printf("PASS session2 (song switch) -> new volume sync (pct=50)\n");
}
}
g_stop = 1;
pthread_join(th, NULL);
unlink(g_sock);