Files
MeowOS-SDK/tuku/test/test_settings_lock_host.c
T
AskaEth ffc41bed9c 图库: 锁屏长按用例去掉恒真断言(M30 评审 P2-1)
「长按清空后仍停在 ACT_LOCK」这条断言测不出清空:进入该段前只输了 "135"
(3 位),而确认在 <4 位时本就 disabled,即使长按没清空也照样停在锁屏 →
断言恒真,与措辞不符。

删掉该断言与其前的空点确认;清空的真正判据仍是紧接着的「输 1357 → 确认 →
解锁」:若没清空,缓冲会是 "135"+"1357" = 7 位错码,必 FAIL。已用变异验证:
临时去掉 lock_clear_all() 后该行确实 FAIL,恢复后 PASS。
2026-11-06 11:50:22 +08:00

356 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(150000); /* 等 text_input 开键盘并 drain 完残留事件 */
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;
}