公开 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。
409 lines
16 KiB
C
409 lines
16 KiB
C
/* test_meow_ui_scroll.c — §12.3 meow_scroll 滚动物理宿主验证:
|
||
* 跟手拖动(track 跟随与 clamp)、速度跟踪→松手 fling(位移序列/停止位置/单调/不越界)、
|
||
* 边界过冲 clamp + 简单回弹、snap。时间推进用真实 usleep(meow_anim_tick 驱动 fling)。
|
||
*
|
||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/draw/activity/ime + 打桩 font/global):
|
||
* gcc -o test_meow_ui_scroll test_meow_ui_scroll.c test_host_stub.c \
|
||
* meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||
* meow_jpeg.c meow_png_stream.c \
|
||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz
|
||
* 运行:./test_meow_ui_scroll
|
||
*/
|
||
#include "meow_ui.h"
|
||
#include "meow_ui_internal.h"
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
|
||
static int g_pass = 0, g_fail = 0;
|
||
#define CHECK(cond, ...) do { \
|
||
if (cond) { g_pass++; } \
|
||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||
} while (0)
|
||
|
||
#define LIST_H 400
|
||
#define ITEM_H 44
|
||
#define COUNT 50
|
||
/* max_scroll = COUNT*ITEM_H - LIST_H = 2200 - 400 = 1800 */
|
||
#define MAXS (COUNT * ITEM_H - LIST_H)
|
||
|
||
static int src(int idx, char *label, int len, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (idx < 0 || idx >= COUNT) return 0;
|
||
snprintf(label, (size_t)len, "row %d", idx);
|
||
return 1;
|
||
}
|
||
|
||
static meow_widget *make_list(void)
|
||
{
|
||
meow_widget *lv = meow_list_create(0, 0, 480, LIST_H, COUNT, src, NULL);
|
||
meow_list_set_item_h(lv, ITEM_H);
|
||
return lv;
|
||
}
|
||
|
||
/* 喂一串向下(正)速度采样,模拟快速下划 */
|
||
static void feed_drag(meow_scroll *s, int step, int n)
|
||
{
|
||
uint64_t now = meow_ui_time_ms();
|
||
for (int i = 0; i < n; i++) {
|
||
meow_scroll_track(s, step, now + (uint64_t)i * 16);
|
||
}
|
||
/* 最后用一个真实稍晚时刻的 track 让采样时间戳真实(保持一致钟) */
|
||
meow_scroll_track(s, 0, meow_ui_time_ms());
|
||
}
|
||
|
||
static void pump_until_idle(meow_scroll *s, int *max_off, int *mono_ok)
|
||
{
|
||
int prev = meow_scroll_offset(s);
|
||
int guard = 0;
|
||
while (meow_scroll_active(s) && guard++ < 300) {
|
||
meow_anim_tick();
|
||
int cur = meow_scroll_offset(s);
|
||
if (*max_off < cur) *max_off = cur;
|
||
if (cur < prev) *mono_ok = 0; /* 下划 fling 应单调不减(回弹段除外:此处只测无回弹) */
|
||
prev = cur;
|
||
usleep(10000);
|
||
}
|
||
meow_anim_tick();
|
||
}
|
||
|
||
static void test_track_follow(void)
|
||
{
|
||
meow_widget *lv = make_list();
|
||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||
CHECK(s != NULL, "wrap");
|
||
CHECK(meow_scroll_offset(s) == 0, "init offset 0");
|
||
meow_scroll_track(s, 10, meow_ui_time_ms());
|
||
CHECK(meow_scroll_offset(s) == 10, "follow +10 got %d", meow_scroll_offset(s));
|
||
/* 超量拖到底:clamp 到 max */
|
||
meow_scroll_track(s, 100000, meow_ui_time_ms());
|
||
CHECK(meow_scroll_offset(s) == MAXS, "clamp to max got %d", meow_scroll_offset(s));
|
||
meow_scroll_free(s);
|
||
meow_widget_free(lv);
|
||
}
|
||
|
||
static void test_fling_no_boundary(void)
|
||
{
|
||
meow_widget *lv = make_list();
|
||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||
/* 从 0 起拖 ~300px,建正初速 */
|
||
uint64_t t = meow_ui_time_ms();
|
||
for (int i = 0; i < 12; i++) meow_scroll_track(s, 25, t + (uint64_t)i * 16);
|
||
int start = meow_scroll_offset(s);
|
||
CHECK(start > 0, "dragged start>0 got %d", start);
|
||
meow_scroll_fling(s);
|
||
CHECK(meow_scroll_active(s) == 1, "fling active");
|
||
int max_off = start, mono = 1, prev = start;
|
||
int guard = 0;
|
||
while (meow_scroll_active(s) && guard++ < 300) {
|
||
meow_anim_tick();
|
||
int cur = meow_scroll_offset(s);
|
||
if (max_off < cur) max_off = cur;
|
||
if (cur < prev) mono = 0;
|
||
prev = cur;
|
||
usleep(10000);
|
||
}
|
||
meow_anim_tick();
|
||
int end = meow_scroll_offset(s);
|
||
CHECK(mono == 1, "monotonic non-decreasing during fling");
|
||
CHECK(end >= start, "fling advances end=%d start=%d", end, start);
|
||
CHECK(end <= MAXS, "no overflow end=%d max=%d", end, MAXS);
|
||
CHECK(meow_scroll_active(s) == 0, "settles idle");
|
||
meow_scroll_free(s);
|
||
meow_widget_free(lv);
|
||
}
|
||
|
||
static void test_fling_boundary_bounce(void)
|
||
{
|
||
meow_widget *lv = make_list();
|
||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||
/* 先拖到近底(~1700) */
|
||
meow_list_set_scroll(lv, 1700);
|
||
/* 快速下划,冲过底界 */
|
||
uint64_t t = meow_ui_time_ms();
|
||
for (int i = 0; i < 8; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16);
|
||
meow_scroll_fling(s);
|
||
int max_off = 0, mono = 1;
|
||
pump_until_idle(s, &max_off, &mono);
|
||
int end = meow_scroll_offset(s);
|
||
CHECK(max_off <= MAXS, "never exceed max, max_off=%d max=%d", max_off, MAXS);
|
||
CHECK(end == MAXS, "bounce settles at bottom end=%d", end); /* 回弹后回到底 */
|
||
CHECK(meow_scroll_active(s) == 0, "idle after bounce");
|
||
meow_scroll_free(s);
|
||
meow_widget_free(lv);
|
||
}
|
||
|
||
static void test_snap(void)
|
||
{
|
||
meow_widget *lv = make_list();
|
||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||
meow_scroll_set_snap(s, ITEM_H); /* 吸附到行边界 */
|
||
/* 慢拖到某非边界位置并给 small velocity → fling 结束 snap 到行倍数 */
|
||
uint64_t t = meow_ui_time_ms();
|
||
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
|
||
meow_scroll_fling(s);
|
||
int max_off = 0, mono = 1;
|
||
pump_until_idle(s, &max_off, &mono);
|
||
int end = meow_scroll_offset(s);
|
||
CHECK((end % ITEM_H) == 0, "snap to item multiple got %d", end);
|
||
CHECK(end <= MAXS, "snap in range %d", end);
|
||
meow_scroll_free(s);
|
||
meow_widget_free(lv);
|
||
}
|
||
|
||
static void test_stop(void)
|
||
{
|
||
meow_widget *lv = make_list();
|
||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||
uint64_t t = meow_ui_time_ms();
|
||
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
|
||
meow_scroll_fling(s);
|
||
CHECK(meow_scroll_active(s) == 1, "fling active (stop test)");
|
||
meow_scroll_stop(s);
|
||
CHECK(meow_scroll_active(s) == 0, "stopped immediately");
|
||
meow_scroll_free(s);
|
||
meow_widget_free(lv);
|
||
}
|
||
|
||
/* ============ 池槽归还回归(M19)============
|
||
* meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽里占一个;旧实现 free 只归还
|
||
* 会话槽、不归还 wrap 槽 → 进程内最多成功 wrap 8 次,之后恒 NULL(图库每次 load_dir 重建,
|
||
* 进 8 个目录后列表丢惯性)。以下用例覆盖:长循环回收、free 后重建、交错不串槽、
|
||
* 池满仍返回 NULL、直接 create 不误清槽、free(NULL)/二次 free 安全。 */
|
||
#define WRAP_POOL 8 /* = meow_ui_scroll.c 的 MEOW_SCROLL_POOL(内部常量,测试按契约固定) */
|
||
|
||
/* 连续 64 次 wrap/free:每次都要拿到可用会话,且绑定当前列表 */
|
||
static void test_wrap_free_recycle(void)
|
||
{
|
||
meow_widget *lv = make_list();
|
||
int ok = 0, first_fail = -1;
|
||
for (int i = 0; i < 64; i++) {
|
||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||
if (!s) { if (first_fail < 0) first_fail = i; continue; }
|
||
meow_list_set_scroll(lv, 0);
|
||
meow_scroll_track(s, 7, meow_ui_time_ms());
|
||
if (meow_scroll_offset(s) == 7) ok++;
|
||
else if (first_fail < 0) first_fail = i; /* 串槽:set/get 不指向本列表 */
|
||
meow_scroll_free(s);
|
||
}
|
||
CHECK(ok == 64, "64 wrap/free cycles all usable, ok=%d first_fail=%d", ok, first_fail);
|
||
meow_widget_free(lv);
|
||
}
|
||
|
||
/* wrap → free → 同一列表再 wrap:必须重新可用 */
|
||
static void test_wrap_after_free(void)
|
||
{
|
||
meow_widget *lv = make_list();
|
||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||
CHECK(s != NULL, "wrap#1 same list");
|
||
meow_scroll_free(s);
|
||
s = meow_scroll_wrap(lv, 0);
|
||
CHECK(s != NULL, "wrap#2 same list after free");
|
||
if (s) {
|
||
meow_list_set_scroll(lv, 0);
|
||
meow_scroll_track(s, 12, meow_ui_time_ms());
|
||
CHECK(meow_scroll_offset(s) == 12, "re-wrapped scroll works got %d", meow_scroll_offset(s));
|
||
meow_scroll_free(s);
|
||
}
|
||
meow_widget_free(lv);
|
||
}
|
||
|
||
/* 8 个同时活跃 → 第 9 个仍返回 NULL(池满不越界);释放其一的槽立刻可再用 */
|
||
static void test_wrap_pool_full(void)
|
||
{
|
||
meow_widget *lv[WRAP_POOL];
|
||
meow_scroll *s[WRAP_POOL];
|
||
int ok = 0;
|
||
for (int i = 0; i < WRAP_POOL; i++) {
|
||
lv[i] = make_list();
|
||
s[i] = meow_scroll_wrap(lv[i], 0);
|
||
if (s[i]) ok++;
|
||
}
|
||
CHECK(ok == WRAP_POOL, "all %d wraps active, ok=%d", WRAP_POOL, ok);
|
||
|
||
meow_widget *extra_lv = make_list();
|
||
meow_scroll *extra = meow_scroll_wrap(extra_lv, 0);
|
||
CHECK(extra == NULL, "9th simultaneous wrap returns NULL (pool full), got %p", (void *)extra);
|
||
|
||
/* 释放中间一个 → 槽立即回收;活着的实例仍各自绑定自己的列表 */
|
||
meow_scroll_free(s[3]);
|
||
s[3] = NULL;
|
||
meow_scroll *again = meow_scroll_wrap(extra_lv, 0);
|
||
CHECK(again != NULL, "wrap succeeds right after freeing one slot");
|
||
if (again) {
|
||
meow_list_set_scroll(extra_lv, 0);
|
||
meow_scroll_track(again, 9, meow_ui_time_ms());
|
||
CHECK(meow_scroll_offset(again) == 9, "new wrap bound to its own list got %d",
|
||
meow_scroll_offset(again));
|
||
meow_list_set_scroll(lv[5], 33);
|
||
CHECK(meow_scroll_offset(s[5]) == 33, "other live wrap keeps its own list got %d",
|
||
meow_scroll_offset(s[5]));
|
||
meow_scroll_free(again);
|
||
}
|
||
for (int i = 0; i < WRAP_POOL; i++) {
|
||
if (s[i]) meow_scroll_free(s[i]);
|
||
meow_widget_free(lv[i]);
|
||
}
|
||
meow_widget_free(extra_lv);
|
||
}
|
||
|
||
/* 交错:两个 wrap 活跃 → free 其一 → 再 wrap,三者不串槽 */
|
||
static void test_wrap_interleaved(void)
|
||
{
|
||
meow_widget *a = make_list(), *b = make_list(), *c = make_list();
|
||
meow_scroll *sa = meow_scroll_wrap(a, 0);
|
||
meow_scroll *sb = meow_scroll_wrap(b, 0);
|
||
CHECK(sa != NULL && sb != NULL, "two wraps (interleaved) a=%p b=%p", (void *)sa, (void *)sb);
|
||
if (!sa || !sb) {
|
||
meow_scroll_free(sa); meow_scroll_free(sb);
|
||
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
|
||
return;
|
||
}
|
||
meow_list_set_scroll(a, 11);
|
||
meow_list_set_scroll(b, 22);
|
||
CHECK(meow_scroll_offset(sa) == 11 && meow_scroll_offset(sb) == 22,
|
||
"distinct ctx a=%d b=%d", meow_scroll_offset(sa), meow_scroll_offset(sb));
|
||
|
||
meow_scroll_free(sa);
|
||
CHECK(meow_scroll_offset(sb) == 22, "remaining wrap intact after freeing first got %d",
|
||
meow_scroll_offset(sb));
|
||
|
||
meow_scroll *sc = meow_scroll_wrap(c, 0);
|
||
CHECK(sc != NULL, "third wrap after freeing first");
|
||
if (sc) {
|
||
meow_list_set_scroll(c, 0);
|
||
meow_scroll_track(sc, 5, meow_ui_time_ms());
|
||
CHECK(meow_scroll_offset(sc) == 5, "third wrap bound to its own list got %d",
|
||
meow_scroll_offset(sc));
|
||
CHECK(meow_scroll_offset(sb) == 22, "second wrap not clobbered by third got %d",
|
||
meow_scroll_offset(sb));
|
||
meow_scroll_free(sc);
|
||
}
|
||
meow_scroll_free(sb);
|
||
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
|
||
}
|
||
|
||
/* ---- 直接 create(非 wrap)的回调:用于验证 free 不误清别人的 wrap 槽 ---- */
|
||
static int g_direct_off;
|
||
static void direct_set(void *c, int off) { (void)c; g_direct_off = off; }
|
||
static int direct_max(void *c) { (void)c; return 1000; }
|
||
static int direct_get(void *c) { (void)c; return g_direct_off; }
|
||
|
||
static void test_direct_create_free_keeps_wrap_slot(void)
|
||
{
|
||
meow_widget *lv = make_list();
|
||
meow_scroll *w = meow_scroll_wrap(lv, 0);
|
||
CHECK(w != NULL, "wrap before direct create");
|
||
meow_scroll *d = meow_scroll_create(direct_set, direct_max, direct_get, NULL, NULL);
|
||
CHECK(d != NULL, "direct create");
|
||
if (d) {
|
||
meow_scroll_track(d, 40, meow_ui_time_ms());
|
||
CHECK(g_direct_off == 40, "direct scroll tracks got %d", g_direct_off);
|
||
meow_scroll_free(d); /* 非 wrap 实例:不得归还/清除任何 wrap 槽 */
|
||
}
|
||
if (w) {
|
||
meow_list_set_scroll(lv, 0);
|
||
meow_scroll_track(w, 10, meow_ui_time_ms());
|
||
CHECK(meow_scroll_offset(w) == 10, "wrap intact after freeing direct instance got %d",
|
||
meow_scroll_offset(w));
|
||
meow_scroll_free(w);
|
||
}
|
||
meow_widget_free(lv);
|
||
}
|
||
|
||
/* free(NULL) 安全;同一实例二次 free 不崩、不清活着的 wrap 槽 */
|
||
static void test_wrap_free_null_and_double(void)
|
||
{
|
||
meow_scroll_free(NULL); /* 必须安全 */
|
||
|
||
meow_widget *a = make_list(), *b = make_list();
|
||
meow_scroll *sa = meow_scroll_wrap(a, 0);
|
||
meow_scroll *sb = meow_scroll_wrap(b, 0);
|
||
CHECK(sa != NULL && sb != NULL, "two wraps (double free) a=%p b=%p", (void *)sa, (void *)sb);
|
||
if (sb) meow_list_set_scroll(b, 44);
|
||
if (sa) meow_scroll_free(sa);
|
||
if (sa) meow_scroll_free(sa); /* 二次 free:重复清零,不得清掉 sb 的 ctx */
|
||
if (sb) {
|
||
CHECK(meow_scroll_offset(sb) == 44, "live wrap intact after double free got %d",
|
||
meow_scroll_offset(sb));
|
||
meow_scroll_free(sb);
|
||
}
|
||
meow_scroll *sc = meow_scroll_wrap(a, 0);
|
||
CHECK(sc != NULL, "wrap still available after free(NULL)/double free");
|
||
if (sc) meow_scroll_free(sc);
|
||
meow_widget_free(a); meow_widget_free(b);
|
||
}
|
||
|
||
/* 虚拟化列表行源(仅需 count 有效,不绘制) */
|
||
static int lv_src2(int idx, meow_list_item *out, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (idx < 0 || idx >= COUNT) return 0;
|
||
memset(out, 0, sizeof *out);
|
||
out->title = "row";
|
||
return 1;
|
||
}
|
||
|
||
/* 槽复用不继承上一个 wrap 的 kind:list → listview → list 交替必须各自生效
|
||
* (free 时清 lv 标记,wrap 时按参数重设;否则复用槽上的旧 kind 会让 set/get 打空) */
|
||
static void test_wrap_slot_kind_reset(void)
|
||
{
|
||
meow_widget *plain = make_list();
|
||
meow_widget *vl = meow_listview_create(0, 0, 480, LIST_H, COUNT, lv_src2, NULL,
|
||
MEOW_LIST_SEL_SINGLE);
|
||
meow_listview_set_item_h(vl, ITEM_H);
|
||
|
||
meow_scroll *s = meow_scroll_wrap(plain, 0);
|
||
CHECK(s != NULL, "wrap plain list");
|
||
if (s) meow_scroll_free(s);
|
||
|
||
s = meow_scroll_wrap(vl, 1);
|
||
CHECK(s != NULL, "wrap listview on recycled slot");
|
||
if (s) {
|
||
meow_listview_set_scroll(vl, 0);
|
||
meow_scroll_track(s, 20, meow_ui_time_ms());
|
||
CHECK(meow_scroll_offset(s) == 20, "listview wrapped scroll works got %d",
|
||
meow_scroll_offset(s));
|
||
meow_scroll_free(s);
|
||
}
|
||
|
||
s = meow_scroll_wrap(plain, 0);
|
||
CHECK(s != NULL, "wrap plain list again on recycled slot");
|
||
if (s) {
|
||
meow_list_set_scroll(plain, 0);
|
||
meow_scroll_track(s, 15, meow_ui_time_ms());
|
||
CHECK(meow_scroll_offset(s) == 15, "plain list after listview slot reuse got %d",
|
||
meow_scroll_offset(s));
|
||
meow_scroll_free(s);
|
||
}
|
||
meow_widget_free(vl);
|
||
meow_widget_free(plain);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
meow_ui_opts o = { 480, 720 };
|
||
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
|
||
/* 池槽回归先跑:新建进程里第一个动 pool 的用例,便于对照「修复前 8/64、修复后 64/64」 */
|
||
test_wrap_free_recycle();
|
||
test_track_follow();
|
||
test_fling_no_boundary();
|
||
test_fling_boundary_bounce();
|
||
test_snap();
|
||
test_stop();
|
||
test_wrap_after_free();
|
||
test_wrap_pool_full();
|
||
test_wrap_interleaved();
|
||
test_direct_create_free_keeps_wrap_slot();
|
||
test_wrap_slot_kind_reset();
|
||
test_wrap_free_null_and_double();
|
||
meow_ui_shutdown();
|
||
printf("scroll: %d pass / %d fail\n", g_pass, g_fail);
|
||
return g_fail ? 1 : 0;
|
||
}
|