Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/plpage_sim.c
T
AskaEth 7c7d891570 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 与容器一致)。
2026-11-04 20:02:28 +08:00

278 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}