Files
MeowOS-For-R3P2/patch_v2/reader_app/test/chapter_smoke.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

276 lines
11 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.
/* 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;
}