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:
1 parent
e87ccdf061
commit
7c7d891570
59 files changed
+5305
-590
No files matched your search
@@ -0,0 +1,275 @@
|
||||
/* test/chapter_smoke.c — os.meow.reader 章节规范解析器宿主单测(纯 C,无 SDK/Freetype)。
|
||||
*
|
||||
* 覆盖(按需求 5 样本 + 跨段 + 中文数字转换):
|
||||
* C1 标准中文数字章(第一章/第二章/第十一章 → 1/2/11,char_off 单调)
|
||||
* C2 阿拉伯章(第1章/第2章/第3章 → 1/2/3)
|
||||
* C3 楔子/序章/尾声/番外(无号,number==-1,char_off 单调)
|
||||
* C4 无章节纯文本(n==0,seg_char_start 仍完整)
|
||||
* C5 开头长书名+简介再进第一章(卷首识别 as entries[0]、章号、防误判正文行 )
|
||||
* C6 跨段:>512KB 书,章落在后段(seg_idx>=1,char_off 跨段单调)
|
||||
* C7 中文数字转换:十一/二十三/一百零一/一千零一/两
|
||||
*
|
||||
* 跑法:build_reader.sh 内 `test/chapter_smoke_host`;本文件只依赖 rdchapter + seg + rdenc。
|
||||
*/
|
||||
#include "rdchapter.h"
|
||||
#include "seg.h"
|
||||
#include "rdenc.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, msg) do { if (cond) { g_pass++; } else { g_fail++; printf("FAIL %s:%d %s\n", __FILE__, __LINE__, msg); } } while (0)
|
||||
|
||||
static char g_dir[600];
|
||||
|
||||
static void write_file(const char *path, const char *data)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
fwrite(data, 1, strlen(data), f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* 建段目录 + 切段,返回段数(<0=错误)。 */
|
||||
static int seg_for(const char *path, const char *base)
|
||||
{
|
||||
rdseg_set_base(base);
|
||||
char dir[600];
|
||||
if (rdseg_dir_for(path, dir, sizeof dir) != 0) return -1;
|
||||
strcpy(g_dir, dir);
|
||||
return rdseg_build(path, RD_ENC_UTF8, dir);
|
||||
}
|
||||
|
||||
static int scan_book(rdchapter *c, const char *basename)
|
||||
{
|
||||
return rdchapter_scan(g_dir, RD_ENC_UTF8, basename, c);
|
||||
}
|
||||
|
||||
static void test_c1(void)
|
||||
{
|
||||
const char *txt = "/tmp/ch_s1.txt";
|
||||
write_file(txt,
|
||||
"第一章 风起\n"
|
||||
"这是第一章的正文内容。\n"
|
||||
"第二章 云涌\n"
|
||||
"这是第二章的正文内容。\n"
|
||||
"第十一章 破晓\n"
|
||||
"这是第十一章的正文内容。\n");
|
||||
CHECK(seg_for(txt, "/tmp/ch_base1") >= 1, "c1 seg build");
|
||||
rdchapter c; rdchapter_init(&c);
|
||||
CHECK(scan_book(&c, "s1") == 0, "c1 scan");
|
||||
CHECK(c.n == 3, "c1 n==3");
|
||||
if (c.n == 3) {
|
||||
CHECK(c.entries[0].number == 1, "c1 #1");
|
||||
CHECK(c.entries[1].number == 2, "c1 #2");
|
||||
CHECK(c.entries[2].number == 11, "c1 #11");
|
||||
CHECK(c.entries[0].char_off == 0, "c1 first at 0 (no preface)");
|
||||
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||
c.entries[1].char_off < c.entries[2].char_off, "c1 char_off monotonic");
|
||||
CHECK(c.entries[0].seg_idx == 0 && c.entries[0].seg_char == 0, "c1 seg0 char0");
|
||||
CHECK(strcmp(c.entries[0].title, "第一章 风起") == 0, "c1 title");
|
||||
CHECK(c.has_pre == 0, "c1 no preface");
|
||||
}
|
||||
rdchapter_free(&c);
|
||||
remove(txt);
|
||||
}
|
||||
|
||||
static void test_c2(void)
|
||||
{
|
||||
const char *txt = "/tmp/ch_s2.txt";
|
||||
write_file(txt,
|
||||
"第1章 起源\n"
|
||||
"正文A。\n"
|
||||
"第2章 发展\n"
|
||||
"正文B。\n"
|
||||
"第3章 终局\n"
|
||||
"正文C。\n");
|
||||
CHECK(seg_for(txt, "/tmp/ch_base2") >= 1, "c2 seg build");
|
||||
rdchapter c; rdchapter_init(&c);
|
||||
CHECK(scan_book(&c, "s2") == 0, "c2 scan");
|
||||
CHECK(c.n == 3, "c2 n==3");
|
||||
if (c.n == 3) {
|
||||
CHECK(c.entries[0].number == 1, "c2 #1");
|
||||
CHECK(c.entries[1].number == 2, "c2 #2");
|
||||
CHECK(c.entries[2].number == 3, "c2 #3");
|
||||
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||
c.entries[1].char_off < c.entries[2].char_off, "c2 char_off monotonic");
|
||||
}
|
||||
rdchapter_free(&c);
|
||||
remove(txt);
|
||||
}
|
||||
|
||||
static void test_c3(void)
|
||||
{
|
||||
const char *txt = "/tmp/ch_s3.txt";
|
||||
write_file(txt,
|
||||
"楔子\n"
|
||||
"一段开篇。\n"
|
||||
"第一章 正传\n"
|
||||
"正文内容。\n"
|
||||
"尾声\n"
|
||||
"后记文字。\n"
|
||||
"番外 归途\n"
|
||||
"番外正文。\n");
|
||||
CHECK(seg_for(txt, "/tmp/ch_base3") >= 1, "c3 seg build");
|
||||
rdchapter c; rdchapter_init(&c);
|
||||
CHECK(scan_book(&c, "s3") == 0, "c3 scan");
|
||||
CHECK(c.n == 4, "c3 n==4");
|
||||
if (c.n == 4) {
|
||||
CHECK(c.entries[0].number == -1, "c3 楔子 number -1");
|
||||
CHECK(strcmp(c.entries[0].title, "楔子") == 0, "c3 楔子 title");
|
||||
CHECK(c.entries[1].number == 1, "c3 第一章 number 1");
|
||||
CHECK(c.entries[2].number == -1, "c3 尾声 number -1");
|
||||
CHECK(c.entries[3].number == -1, "c3 番外 number -1");
|
||||
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||
c.entries[1].char_off < c.entries[2].char_off &&
|
||||
c.entries[2].char_off < c.entries[3].char_off, "c3 char_off monotonic");
|
||||
}
|
||||
rdchapter_free(&c);
|
||||
remove(txt);
|
||||
}
|
||||
|
||||
static void test_c4(void)
|
||||
{
|
||||
const char *txt = "/tmp/ch_s4.txt";
|
||||
write_file(txt,
|
||||
"这是一个没有章节标题的长篇小说文本。\n"
|
||||
"它只有连续不断的段落,没有任何章回节的标识。\n"
|
||||
"所有的文字都是纯粹的故事内容。\n");
|
||||
CHECK(seg_for(txt, "/tmp/ch_base4") >= 1, "c4 seg build");
|
||||
rdchapter c; rdchapter_init(&c);
|
||||
CHECK(scan_book(&c, "s4") == 0, "c4 scan");
|
||||
CHECK(c.n == 0, "c4 no chapters");
|
||||
CHECK(c.nsegs == 1, "c4 nsegs==1");
|
||||
CHECK(c.seg_char_start && c.seg_char_start[0] == 0 && c.seg_char_start[1] > 0, "c4 seg_char_start");
|
||||
rdchapter_free(&c);
|
||||
remove(txt);
|
||||
}
|
||||
|
||||
static void test_c5(void)
|
||||
{
|
||||
const char *txt = "/tmp/ch_s5.txt";
|
||||
write_file(txt,
|
||||
"《大漠孤烟》这是一本很长的书名,作者著。\n"
|
||||
"简介:这是一个故事的开头,讲述少年成长。\n"
|
||||
"第一章风雨欲来,全书开场,命运由此转折。\n"
|
||||
"第一章 离家\n"
|
||||
"正文内容。\n"
|
||||
"第二章 远行\n"
|
||||
"正文内容。\n");
|
||||
CHECK(seg_for(txt, "/tmp/ch_base5") >= 1, "c5 seg build");
|
||||
rdchapter c; rdchapter_init(&c);
|
||||
CHECK(scan_book(&c, "sample5") == 0, "c5 scan");
|
||||
CHECK(c.n == 3, "c5 n==3 (preface + 2 chapters)");
|
||||
if (c.n == 3) {
|
||||
CHECK(c.has_pre == 1, "c5 has preface");
|
||||
CHECK(c.entries[0].number == -1, "c5 preface number -1");
|
||||
CHECK(strcmp(c.entries[0].title, "sample5") == 0, "c5 preface title = basename");
|
||||
CHECK(c.entries[0].char_off == 0, "c5 preface char_off 0");
|
||||
CHECK(c.entries[1].number == 1 && c.entries[2].number == 2, "c5 chapter numbers 1/2");
|
||||
CHECK(c.entries[1].char_off > 0, "c5 ch1 after preface");
|
||||
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||
c.entries[1].char_off < c.entries[2].char_off, "c5 monotonic");
|
||||
/* 防误判:正文行「第一章风雨欲来,全书开场。」带两个正文标点 → 未被识别为章 */
|
||||
CHECK(c.entries[1].seg_char > 0, "c5 decoy line not counted (ch1 seg_char>0)");
|
||||
}
|
||||
rdchapter_free(&c);
|
||||
remove(txt);
|
||||
}
|
||||
|
||||
static void test_c6(void)
|
||||
{
|
||||
/* 跨段:写 ~600KB filler,在 ~600KB 处放一个章 → 应落在后段(seg_idx>=1) */
|
||||
const char *txt = "/tmp/ch_s6.txt";
|
||||
FILE *f = fopen(txt, "wb");
|
||||
const char *para = "这是一段用于填充页面的中文正文,包含标点,中文English混合,反复出现,占据空间。\n";
|
||||
size_t plen = strlen(para);
|
||||
size_t total = 0;
|
||||
while (total < 600 * 1024) {
|
||||
fwrite(para, 1, plen, f); total += plen;
|
||||
}
|
||||
fwrite("第二百章 跨段检测\n", 1, strlen("第二百章 跨段检测\n"), f);
|
||||
const char *tail = "这是跨段之后的一小段正文。\n";
|
||||
fwrite(tail, 1, strlen(tail), f);
|
||||
fclose(f);
|
||||
CHECK(seg_for(txt, "/tmp/ch_base6") >= 2, "c6 seg build >=2");
|
||||
rdchapter c; rdchapter_init(&c);
|
||||
CHECK(scan_book(&c, "s6") == 0, "c6 scan");
|
||||
/* 前置 filler(600KB 无章节)→ entries[0]=卷首;真正章节在后段 */
|
||||
CHECK(c.n == 2, "c6 n==2 (preface + 1 chapter)");
|
||||
if (c.n == 2) {
|
||||
CHECK(c.entries[0].number == -1, "c6 preface number -1");
|
||||
CHECK(c.entries[1].number == 200, "c6 number 200");
|
||||
CHECK(c.entries[1].seg_idx >= 1, "c6 chapter in later segment");
|
||||
CHECK(c.entries[1].seg_idx < c.nsegs, "c6 seg_idx in range");
|
||||
CHECK(c.entries[1].seg_char >= 0, "c6 seg_char >=0");
|
||||
CHECK(c.seg_char_start[0] == 0 &&
|
||||
c.seg_char_start[0] < c.seg_char_start[1] &&
|
||||
c.seg_char_start[c.nsegs - 1] < c.seg_char_start[c.nsegs], "c6 seg_char_start monotonic");
|
||||
/* 跨段 char_off:章首偏移应在段 1 起点之后 */
|
||||
CHECK(c.entries[1].char_off >= c.seg_char_start[c.entries[1].seg_idx], "c6 char_off within seg");
|
||||
}
|
||||
rdchapter_free(&c);
|
||||
remove(txt);
|
||||
}
|
||||
|
||||
static void test_c7(void)
|
||||
{
|
||||
CHECK(rdchapter_cn_num("十一", 6) == 11, "c7 十一=11");
|
||||
CHECK(rdchapter_cn_num("二十三", 9) == 23, "c7 二十三=23");
|
||||
CHECK(rdchapter_cn_num("一百零一", 12) == 101, "c7 一百零一=101");
|
||||
CHECK(rdchapter_cn_num("一千零一", 12) == 1001, "c7 一千零一=1001");
|
||||
CHECK(rdchapter_cn_num("两", 3) == 2, "c7 两=2");
|
||||
CHECK(rdchapter_cn_num("一百", 6) == 100, "c7 一百=100");
|
||||
CHECK(rdchapter_cn_num("foobar", 6) == -1, "c7 invalid -1");
|
||||
}
|
||||
|
||||
static void test_c8(void)
|
||||
{
|
||||
/* R3.4 英文章节:文字数字(one/twenty-one/two hundred fifty)+ 罗马(IV)+ 拒绝无空格(ChapterX) */
|
||||
const char *txt = "/tmp/ch_s8.txt";
|
||||
write_file(txt,
|
||||
"Chapter one\n"
|
||||
"正文。\n"
|
||||
"Chapter IV\n"
|
||||
"正文。\n"
|
||||
"Chapter twenty-one\n"
|
||||
"正文。\n"
|
||||
"Chapter two hundred fifty\n"
|
||||
"正文。\n"
|
||||
"ChapterX\n"
|
||||
"正文。\n"
|
||||
"Chapter twelve\n"
|
||||
"正文。\n");
|
||||
CHECK(seg_for(txt, "/tmp/ch_base8") >= 1, "c8 seg build");
|
||||
rdchapter c; rdchapter_init(&c);
|
||||
CHECK(scan_book(&c, "s8") == 0, "c8 scan");
|
||||
/* ChapterX 不认(无空格);其余 5 个认 */
|
||||
CHECK(c.n == 5, "c8 n==5");
|
||||
if (c.n == 5) {
|
||||
static const int exp[5] = { 1, 4, 21, 250, 12 };
|
||||
for (int i = 0; i < 5; i++) CHECK(c.entries[i].number == exp[i], "c8 word number value");
|
||||
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
|
||||
c.entries[1].char_off < c.entries[2].char_off &&
|
||||
c.entries[2].char_off < c.entries[3].char_off &&
|
||||
c.entries[3].char_off < c.entries[4].char_off, "c8 char_off monotonic");
|
||||
CHECK(strcmp(c.entries[0].title, "Chapter one") == 0, "c8 title Chapter one");
|
||||
CHECK(strcmp(c.entries[3].title, "Chapter two hundred fifty") == 0, "c8 title two hundred fifty");
|
||||
}
|
||||
rdchapter_free(&c);
|
||||
remove(txt);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_c1();
|
||||
test_c2();
|
||||
test_c3();
|
||||
test_c4();
|
||||
test_c5();
|
||||
test_c6();
|
||||
test_c7();
|
||||
test_c8();
|
||||
printf("chapter_smoke: %d pass, %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user