Files
MeowOS-SDK/tuku/test/test_settings_lock_host.c
T
AskaEth 27a3750e36 SDK: 同步主仓 M15/M16/bug3/MD3 修复 — 时钟 ABI 补全/IME 冷却窗/列表行高派生/形状 token/翻页键
公开 SDK 落后主仓的一批 bugfix(主仓是固件构建源,这些修复已在设备上跑过)。
本次只做「主仓 → 公开仓」单向同步,图像栈方向由主仓侧另一单处理。

时钟 ABI(M16 第二弹,原文件自认「残余未修」):
- meow_ui_time.c 147→345 行:futex 相对超时接管(__timedwait/_cp)、
  timerfd_create/settime/gettime 的 itimerspec 64↔32 双向转换、
  __clock_settime64 越界返 ENOTSUP,另有 4 个纯换算函数
- meow_ui_internal.h:struct meow_time32 / meow_itimerspec32 契约移入内部头
- CLOCK_ABI.md:补接管机制、验证配方与 objdump 证据
- 新增 test_meow_time64.c

IME bug3(成对才有意义):
- meow_ui_ime.c:IME_TAP_COOLDOWN_MS 200 冷却窗 + 打开时二次 drain
- meow_ui_input.c:meow_input_touch_reset() 并在 drain 内调用(复位触摸状态机)
- test_ime_fix.c:新增用例④ stray-tap

列表行高(M15,修用户可见 bug):
- meow_ui_widget.c:listv_item_h() 由主题字体行高派生,不再写死 56
  (两行列表 62 > 56 会让首行标题被裁剪守卫整条跳过)
- include/meow_ui_widgets.h:新增 meow_listview_item_h()
- 新增 test_meow_ui_listview_rowh.c

其余:meow_select.c / meow_ui_dialog.c 统一 MD3 形状 token;
include/meow_ipc.h 补 MSG_KEY_PAGEUP/PAGEDOWN 枚举;
meow_ui_sys.c / meow_cachegov.c 取时统一走 meow_ui_time_ms()。

测试辅助函数标定:tuku/test/test_settings_lock_host.c 的注入前等待 150ms→350ms。
该等待按修复前 IME 标定,新冷却窗下首键 dt_open≈114ms 会被当迟到抬起吞掉,
只录到 "68" 两位。仅改等待时长,断言原样保留。

验证:tuku 四套脚本全绿(run_tests 644 PASS / zip 106 / cbz 296 / webtoon PASS);
12 个源文件与主仓逐字节一致;sdk/build_sdk.sh 34 成员;
tuku/build_tuku.sh 产出静态 MIPS32r2 ELF。
2026-11-06 21:31:54 +08:00

358 lines
17 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_settings_lock_host.c — 设置页/锁屏与 SDK 骨架的宿主集成测试(可选,需 SDK 源码树)。
*
* 目的:钉住 M3 骨架评审点名的两条坑(M5 桩注释①/②),这两条只有把真实 SDK
* view/activity/widget 链起来才能验证:
* ① 非根页控件必须在 on_enter 建(此时 activity 栈顶=本页)——否则 owner 绑到
* ACT_LIBRARY,跨页命中泄漏;
* ② 锁屏必须在自身 view 的 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c
* 的 on_swipe),否则左缘返回会把 ACT_LOCK page_pop 掉、PIN 被绕过。
* 另外覆盖:设置页回到书库(topbar 返回)、控件跨进出复用不重建、阅读方向经
* mr_reader_set_direction 切换、lock_view_should_show() 随 PIN 状态变化、
* 主题模式切换后整帧重绘不崩,以及两条端到端路径:
* - **锁屏页内自绘 12 键数字键盘**(1-9 / 退格·0·确认):错 PIN 留在锁屏 → 对 PIN
* page_pop 回书库;位数不足确认不可用、超长输入截断、退格/长按清空、错误后清空重输;
* 键高/间距/屏底 28px 热区几何契约;长按退格走真实 SDK opt-in 路径(未注册回调不产生
* MEOW_EV_LONG_PRESS = M29 评审 P2 的根因,注册后才清空);
* - 设置页两遍输入开启 PIN(仍走真实 MEOW_IME_NUM 键盘注入)。
*
* 编译:见 test/run_tests.sh —— 它把本测试、settings/lock/pin 源码与 SDK 源码
* (-DMEOW_UI_HOST_TEST,附 test_host_stub.c 打桩 font/global)一起编;SDK 对象清单在脚本里。
* 只发布 meow_ui.a(无 SDK 源码)的交付环境,run_tests.sh 自动 SKIP 本测试。
*/
#include "tuku_internal.h"
#include "logic/pin.h"
#include "meow_ui_internal.h" /* meow_widget_owner(宿主测试专用内部头) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
static int g_fail;
#define CHECK(cond, ...) do { \
if (cond) { printf(" PASS\n"); } \
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* app.c 的导航上报在测试里不需要网络侧实现。 */
void mr_nav_report(void) {}
static int g_lib_enters;
static void lib_enter(meow_view *v, void *c) { (void)v; (void)c; g_lib_enters++; }
static void lib_leave(meow_view *v, void *c) { (void)v; (void)c; }
/* 清掉本次测试写入的三个小配置并删临时目录(meow_config 每键一文件)。 */
static void cleanup_cfg(const char *dir)
{
static const char *keys[3] = { "pin.enabled.cfg", "pin.hash.cfg", "pin.salt.cfg" };
char p[512];
for (int i = 0; i < 3; i++) {
snprintf(p, sizeof p, "%s/%s", dir, keys[i]);
unlink(p);
}
rmdir(dir);
}
/* ============================================================ */
/* 数字键盘注入(宿主没有真实触摸):按 MEOW_IME_NUM 键位投递 TAP,关闭键盘后由被测代码校验。
* NUM 布局(sdk/meow_ui_ime.c build_kb):KB_TOP=420;首行 470 是符号行 "-/.:*#+=",
* 数字九键行为 518/566/614(x0=70/190/310,键宽 100),底行(ESC|0|退格+OK)为 662;
* 点 y<420 的背景=关闭键盘。 */
/* ============================================================ */
typedef struct { const char *digits; int passes; } pin_job;
static void key_tap(char d)
{
int row, col;
if (d == '0') { row = 3; col = 1; }
else {
int v = d - '1';
if (v < 0 || v > 8) return;
row = v / 3;
col = v % 3;
}
/* 数字行 y=518/566/614(首行 470 是符号行 "-/.:*#+="),底行 662;列心 120/240/360。 */
meow_input_inject(MEOW_EV_TAP, 70 + col * 120 + 50, 518 + row * 48 + 18);
}
static void *pin_injector(void *arg)
{
pin_job *job = (pin_job *)arg;
usleep(350000); /* 等 text_input 开键盘 + drain 完残留事件,并等过
* IME_TAP_COOLDOWN_MS(200) 冷却窗(bug3):窗内 TAP 被
* 当迟到抬起吞掉,150ms 时首键 dt_open≈114ms 会丢 */
for (int p = 0; p < job->passes; p++) {
if (p) usleep(300000); /* 两遍输入之间等第二遍开键盘 */
for (const char *s = job->digits; *s; s++) {
key_tap(*s);
usleep(50000);
}
usleep(50000);
meow_input_inject(MEOW_EV_TAP, 240, 100); /* y<420:关闭键盘(非空→校验) */
}
return NULL;
}
/* ============================================================ */
/* 锁屏页内自绘 12 键数字键盘(宿主没有真实触摸):按 widget 矩形中心投递 TAP。
* 键下标 = lock.c 创建序:0..8 = 1..9,9 = 退格,10 = 0,11 = 确认(LK_K*)。
* ============================================================ */
enum { LK_K1 = 0, LK_K9 = 8, LK_KBACK = 9, LK_K0 = 10, LK_KOK = 11 };
static int lock_key_center(meow_view *lock, int idx, int *cx, int *cy)
{
int r[4] = { 0 };
if (!lock || idx < 0 || idx >= lock->nwidgets || !lock->widgets[idx]) return 0;
meow_widget_rect(lock->widgets[idx], r);
*cx = r[0] + r[2] / 2;
*cy = r[1] + r[3] / 2;
return 1;
}
static void lock_tap_key(meow_view *lock, int idx)
{
int cx, cy;
if (lock_key_center(lock, idx, &cx, &cy))
meow_view_dispatch_event(MEOW_EV_TAP, cx, cy);
}
static int lock_key_idx(char d)
{
return (d == '0') ? LK_K0 : (d - '1');
}
static void lock_type(meow_view *lock, const char *digits)
{
for (const char *s = digits; *s; s++) lock_tap_key(lock, lock_key_idx(*s));
}
/* app.c 注册的 no-op 长按回调(事件先经 view 分发,锁屏页消费;其它页落到这里)。 */
static void app_long_press_noop(int x, int y) { (void)x; (void)y; }
static void settings_tap_pin_row(void){
/* 布局:sb + 56 顶栏 + 6 + 段头34 + 方向行64 + 12 + 段头34 → PIN 列表首行中心 +32 */
int y = meow_ui_statusbar_height() + 56 + 6 + 34 + 64 + 12 + 34 + 32;
pin_job job = { "2468", 2 };
pthread_t th;
if (pthread_create(&th, NULL, pin_injector, &job) != 0) return;
meow_view_dispatch_event(MEOW_EV_TAP, 200, y);
pthread_join(th, NULL);
}
int main(void)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = ".";
char tmpl[512];
snprintf(tmpl, sizeof tmpl, "%s/mr_settings_host_XXXXXX", base);
char *data = mkdtemp(tmpl);
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
setenv("MEOW_DATA_DIR", data, 1);
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
static const meow_view_hooks lh = { .on_enter = lib_enter, .on_leave = lib_leave };
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
meow_view *settings = settings_view_create();
meow_view *lock = lock_view_create();
int sb = meow_ui_statusbar_height();
meow_view_page_push(ACT_LIBRARY);
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "start at library");
/* ---- 设置页:on_enter 建控件 + owner 归属 ---- */
meow_view_page_push(ACT_SETTINGS);
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active");
CHECK(settings->nwidgets == 5, "settings built 5 widgets (got %d)", settings->nwidgets);
int owner_ok = 1;
for (int i = 0; i < settings->nwidgets; i++)
if (meow_widget_owner(settings->widgets[i]) != ACT_SETTINGS) owner_ok = 0;
CHECK(owner_ok, "settings widgets owned by ACT_SETTINGS (no cross-page leak)");
/* 布局几何:顶栏贴状态栏、三组列表不越界、页脚在屏内。 */
int r_tb[4] = { 0 }, r_about[4] = { 0 };
meow_widget_rect(settings->widgets[0], r_tb);
meow_widget_rect(settings->widgets[3], r_about);
CHECK(r_tb[1] == sb && r_tb[3] == 56, "topappbar sits below statusbar");
CHECK(r_about[1] + r_about[3] < meow_ui_height() - 60,
"about list leaves room for footer (bottom=%d)", r_about[1] + r_about[3]);
printf(" geometry: topbar y=%d, about list y=%d h=%d (screen %dx%d)\n",
r_tb[1], r_about[1], r_about[3], meow_ui_width(), meow_ui_height());
/* 阅读方向行:段头 34 + 行中心。 */
int dir_y = sb + 56 + 6 + 34 + 32;
int d0 = mr_reader_get_direction();
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 120, dir_y) == 1,
"tap on direction row consumed by settings on_event");
CHECK(mr_reader_get_direction() != d0,
"direction toggled through mr_reader_set_direction");
/* topbar 返回 → page_pop 回书库 */
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 22, sb + 28) == 1, "back tap consumed");
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back popped to library");
CHECK(g_lib_enters == 2, "library re-entered after pop (enters=%d)", g_lib_enters);
/* 再次进入:控件复用(不在 on_enter 重复创建/泄漏) */
meow_view_page_push(ACT_SETTINGS);
CHECK(settings->nwidgets == 5, "settings widgets reused (got %d)", settings->nwidgets);
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active again");
/* 设置页不消费 SWIPE:交给 app.c 的 page_pop */
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 200) == 0,
"settings leaves SWIPE to app.c (back = pop)");
meow_view_page_pop(); /* 手动模拟 app.c 的返回行为,回到书库 */
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "manual pop back to library");
/* ---- 锁屏:PIN 状态驱动 + SWIPE 拦截(坑②) ---- */
mr_pin_disable();
CHECK(lock_view_should_show() == 0, "lock hidden when PIN disabled");
CHECK(mr_pin_set("1357") == MR_PIN_OK, "enable PIN");
CHECK(lock_view_should_show() == 1, "lock shown when PIN enabled");
meow_view_page_push(ACT_LOCK);
CHECK(meow_ui_activity_active() == ACT_LOCK, "lock active");
CHECK(lock->nwidgets == 12, "lock built 12-key pad on enter (got %d)", lock->nwidgets);
int lock_owner_ok = 1;
for (int i = 0; i < lock->nwidgets; i++)
if (meow_widget_owner(lock->widgets[i]) != ACT_LOCK) lock_owner_ok = 0;
CHECK(lock_owner_ok, "pad keys owned by ACT_LOCK (no cross-page leak)");
/* 几何契约:键 ≥64px 高、间距 ≥8px、不覆盖屏底 28px 上滑热区、全在屏内。 */
int rk[12][4];
for (int i = 0; i < 12; i++) meow_widget_rect(lock->widgets[i], rk[i]);
int geo_ok = 1, gap_ok = 1, hot_ok = 1;
for (int i = 0; i < 12; i++) {
if (rk[i][2] < 64 || rk[i][3] < 64) geo_ok = 0;
if (rk[i][0] < 0 || rk[i][0] + rk[i][2] > meow_ui_width()) geo_ok = 0;
if (rk[i][1] + rk[i][3] > meow_ui_height() - 28) hot_ok = 0;
}
/* 列间距(行内 0/1/2)与行间距(行 0 与行 3:下标 0 与 9)。 */
if (rk[1][0] - (rk[0][0] + rk[0][2]) < 8) gap_ok = 0;
if (rk[2][0] - (rk[1][0] + rk[1][2]) < 8) gap_ok = 0;
if (rk[9][1] - (rk[0][1] + rk[0][3]) < 8) gap_ok = 0;
CHECK(geo_ok, "pad keys >=64px and within screen");
CHECK(gap_ok, "pad gaps >=8px");
CHECK(hot_ok, "pad leaves the 28px bottom swipe hot zone");
printf(" pad geometry: key %dx%d, pad top=%d bottom=%d (screen %dx%d)\n",
rk[0][2], rk[0][3], rk[0][1], rk[11][1] + rk[11][3],
meow_ui_width(), meow_ui_height());
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 240) == 1,
"lock consumes SWIPE (left-edge back cannot bypass PIN)");
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after swipe");
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 5, 400) == 1, "lock consumes stray taps");
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after stray tap");
CHECK(mr_pin_verify("1357") == 1, "stored PIN verifies");
CHECK(mr_pin_verify("1358") == 0, "wrong PIN rejected");
/* 空输入 / 位数不足:确认不可用(不误判、不退出锁屏)。 */
lock_tap_key(lock, LK_KOK);
CHECK(meow_ui_activity_active() == ACT_LOCK, "empty confirm keeps lock");
lock_type(lock, "135");
lock_tap_key(lock, LK_KOK);
CHECK(meow_ui_activity_active() == ACT_LOCK, "confirm disabled below 4 digits");
/* 第 4 位后确认可用:1-3-5-7 → 回书库。 */
lock_tap_key(lock, lock_key_idx('7'));
lock_tap_key(lock, LK_KOK);
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "4-digit correct PIN pops lock to library");
CHECK(meow_ime_active() == 0, "in-page pad never opens the system IME");
/* 错 PIN:留在锁屏 + 清空;再输对 → 解锁(证明错误后缓冲已清空)。 */
meow_view_page_push(ACT_LOCK);
lock_type(lock, "1358");
lock_tap_key(lock, LK_KOK);
CHECK(meow_ui_activity_active() == ACT_LOCK, "wrong PIN keeps lock (retry allowed)");
lock_type(lock, "1357");
lock_tap_key(lock, LK_KOK);
CHECK(meow_ui_activity_active() == ACT_LIBRARY,
"retry after wrong PIN unlocks (buffer cleared on error)");
/* 8 位上限:9 个键只收前 8 位;退格 4 次回到 "1357" → 解锁。 */
meow_view_page_push(ACT_LOCK);
lock_type(lock, "135713579");
for (int i = 0; i < 4; i++) lock_tap_key(lock, LK_KBACK);
lock_tap_key(lock, LK_KOK);
CHECK(meow_ui_activity_active() == ACT_LIBRARY,
"9th digit ignored (max 8) + backspace removes digits");
/* 退格空缓冲边界:多按不崩,随后正常输入。 */
meow_view_page_push(ACT_LOCK);
for (int i = 0; i < 3; i++) lock_tap_key(lock, LK_KBACK);
lock_type(lock, "1357");
lock_tap_key(lock, LK_KOK);
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "backspace on empty buffer is a no-op");
/* 长按退格清空(M29 评审 P2-1)走真实 SDK 路径:
* SDK 长按是 opt-in —— 未注册回调时 meow_input_long_press_poll 恒返回 0,
* MEOW_EV_LONG_PRESS 永远不会产生(真机上「长按清空」只会退化成单删一位,P2 的根因);
* app.c 在锁屏为顶层页时注册 no-op 回调后,按住 700ms(≥500 阈值)应真正产出 LONG_PRESS,
* 并按主循环顺序先交 view 分发(锁屏页消费),且其抬手不再补一个 TAP。 */
meow_view_page_push(ACT_LOCK);
int bx = 0, by = 0;
CHECK(lock_key_center(lock, LK_KBACK, &bx, &by), "backspace key has a rect");
meow_ui_set_long_press_cb(NULL);
meow_input_touch_set(1, bx, by, 700);
CHECK(meow_input_long_press_poll() == 0,
"no LONG_PRESS while callback unregistered (opt-in gate = P2 root cause)");
meow_input_touch_release();
meow_input_drain(); /* 未成长按的这次按压,抬手会补一个 TAP,清掉 */
lock_type(lock, "135");
meow_ui_set_long_press_cb(app_long_press_noop); /* 同 app.c:锁屏为顶层页时注册 */
meow_input_touch_set(1, bx, by, 700);
CHECK(meow_input_long_press_poll() == 1, "registered long-press crosses the 500ms threshold");
meow_input_ev lp = { 0, 0, 0 };
CHECK(meow_input_pop(&lp) == 1 && lp.type == MEOW_EV_LONG_PRESS &&
lp.a == bx && lp.b == by, "LONG_PRESS queued at the pressed backspace key");
CHECK(meow_view_dispatch_event(MEOW_EV_LONG_PRESS, lp.a, lp.b) == 1,
"lock view consumes LONG_PRESS before the no-op callback");
meow_input_touch_release();
CHECK(meow_input_pop(&lp) == 0, "long-press release does not emit a stray TAP");
/* 「到底清没清空」不在这里断言:此刻缓冲只有 "135"(3 位),而确认在 <4 位时本就
* disabled,任何「仍停在 ACT_LOCK」的判断都恒真、测不出清空(M30 评审 P2-1)。
* 真正的判据是下一段:若没清空,缓冲会变成 "135"+"1357" = 7 位错码 → 校验失败。 */
lock_type(lock, "1357");
lock_tap_key(lock, LK_KOK);
CHECK(meow_ui_activity_active() == ACT_LIBRARY,
"unlock after long-press clear (7-digit buffer would fail if not cleared)");
/* 解锁后 app.c 关掉长按:不得把长按语义泄漏到书库/阅读器(按住稍久的点击仍算 TAP)。 */
meow_ui_set_long_press_cb(NULL);
meow_input_touch_set(1, bx, by, 700);
CHECK(meow_input_long_press_poll() == 0, "long-press off again after leaving the lock page");
meow_input_touch_release();
meow_input_drain();
/* ---- 设置页两遍输入开启 PIN(MEOW_IME_NUM) ---- */
mr_pin_disable();
meow_view_page_push(ACT_SETTINGS);
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active for PIN enable");
settings_tap_pin_row();
CHECK(mr_pin_enabled() == 1, "two-pass entry enabled the PIN");
CHECK(mr_pin_verify("2468") == 1, "PIN entered in settings verifies");
meow_view_page_pop();
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back from settings to library");
/* 主题切换:app.c listener 整帧 invalidate;页面按 meow_theme_get() 重画不崩。 */
meow_theme_set_mode(MEOW_THEME_LIGHT);
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
meow_theme_set_mode(MEOW_THEME_DARK);
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
CHECK(1, "theme switch repaint does not crash");
mr_pin_disable();
meow_view_free(lock);
meow_view_free(settings);
meow_view_free(lib);
meow_ui_shutdown();
cleanup_cfg(data);
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
g_fail ? "see failures above" : "integration checks passed");
return g_fail ? 1 : 0;
}