/* test_mp_keyq.c — M64 r1 P2-1 宿主回归:连续 tap 不被合并 + 顺序保证 + 有界预算。 * * 纯宿主测试:不碰 /dev/input、不发 /api/key、不启面板服务。注入 sink 是记录器, * 另有一个「电平采样器」线程模拟 launcher 的读键方式(EVIOCGKEY 状态采样:只有从 0→1 * 的跳变才算一次按键,见 meow_launcherd/keys.c kp_poll + main_menu.c 每轮采样)。 * * 编译运行(在 patch_v2/meowpanel 下): * cc -O1 -Wall -Wextra -I src -pthread test_mp_keyq.c src/mp_keyq.c -o ~/tmp/test_mp_keyq * ~/tmp/test_mp_keyq * * 反向控制:同一个「5 连击」分别用 * ① 修复前的实现方式(每个按键一个 detached 线程并发 press/sleep/release)→ 采样器只看到 1 次按下; * ② 修复后的队列(mp_keyq_submit 一个作业 / 5 个单键作业)→ 采样器看到 5 次按下。 */ #include #include #include #include #include "mp_keyq.h" static int g_fail; #define CHECK(cond, ...) do { \ if (!(cond)) { printf(" FAIL: "); printf(__VA_ARGS__); printf("\n"); g_fail = 1; } \ else { printf(" PASS: "); printf(__VA_ARGS__); printf("\n"); } \ } while (0) /* ---- 被注入的「设备」:单一电平(key down/up) ---- */ static pthread_mutex_t g_dev_mu = PTHREAD_MUTEX_INITIALIZER; static int g_level; /* 0=抬起 1=按下 */ static void dev_set(int v) { pthread_mutex_lock(&g_dev_mu); g_level = v; pthread_mutex_unlock(&g_dev_mu); } static int dev_get(void) { pthread_mutex_lock(&g_dev_mu); int v = g_level; pthread_mutex_unlock(&g_dev_mu); return v; } /* ---- 记录器 sink:按调用顺序记录,并驱动设备电平 ---- */ #define REC_MAX 64 static int g_rec_code[REC_MAX]; static int g_rec_n; static void rec_sink(int fd, int code, int press_ms) { (void)fd; dev_set(1); usleep((useconds_t)(press_ms > 0 ? press_ms : 80) * 1000); dev_set(0); /* 记录顺序与时序(在 release 之后记,避免与记录数组本身的并发) */ pthread_mutex_lock(&g_dev_mu); if (g_rec_n < REC_MAX) g_rec_code[g_rec_n] = code; g_rec_n++; pthread_mutex_unlock(&g_dev_mu); } /* ---- 电平采样器:2ms 一次,只数 0→1 的跳变(模拟 launcher kp_poll) ---- */ static volatile int g_samp_stop, g_samp_edges; static void *sampler(void *arg) { (void)arg; int prev = 0; while (!g_samp_stop) { int cur = dev_get(); if (cur && !prev) g_samp_edges++; prev = cur; usleep(2 * 1000); } return NULL; } static void rec_reset(void) { pthread_mutex_lock(&g_dev_mu); g_rec_n = 0; memset(g_rec_code, 0, sizeof g_rec_code); pthread_mutex_unlock(&g_dev_mu); g_samp_edges = 0; dev_set(0); } /* 等队列把 n 个作业执行完(有界等待,测试用)。 */ static int wait_done(int base, int n, int timeout_ms) { for (int w = 0; w < timeout_ms; w += 5) { if (mp_keyq_done_count() - base >= n) return 1; usleep(5 * 1000); } return 0; } /* 修复前的做法:每个按键一个 detached 线程,press→sleep→release 并发跑。 */ static void *legacy_thread(void *arg) { int ms = (int)(long)arg; dev_set(1); usleep((useconds_t)ms * 1000); dev_set(0); return NULL; } int main(void) { pthread_t st; pthread_create(&st, NULL, sampler, NULL); usleep(10 * 1000); /* 让采样器先跑起来 */ printf("== 反向控制:修复前的并发注入,5 连击只看到 1 次按下 ==\n"); rec_reset(); { pthread_t t[5]; for (int i = 0; i < 5; i++) pthread_create(&t[i], NULL, legacy_thread, (void *)(long)40); usleep(120 * 1000); /* 5 个线程同时 press、40ms 后各自 release */ for (int i = 0; i < 5; i++) pthread_join(t[i], NULL); } usleep(20 * 1000); CHECK(g_samp_edges >= 1 && g_samp_edges < 5, "修复前:5 个重叠 press 被采样成 %d 次按下(< 5,正是滑动只走 1 步的原因)", g_samp_edges); printf("== 修复后:5 个单键作业串行 → 5 次按下,顺序一致 ==\n"); rec_reset(); int base = mp_keyq_done_count(); int codes5[5] = { 114, 115, 163, 164, 165 }; for (int i = 0; i < 5; i++) { int fd = i; int rc = mp_keyq_submit(&fd, &codes5[i], (int[]){ 40 }, 1, rec_sink); CHECK(rc == MP_KEYQ_OK, "第 %d 个 tap 入队成功(请求线程不等待注入完成)", i + 1); } CHECK(wait_done(base, 5, 2000), "5 个作业在预算内执行完"); usleep(20 * 1000); CHECK(g_samp_edges == 5, "采样器看到 5 次按下(修复前只有 1 次)"); { int ok = 1; for (int i = 0; i < 5; i++) if (g_rec_code[i] != codes5[i]) ok = 0; CHECK(ok && g_rec_n == 5, "记录顺序 = 提交顺序(114,115,163,164,165)"); } printf("== 修复后:批次内部顺序 + 与下一个请求不重叠(不丢步) ==\n"); rec_reset(); base = mp_keyq_done_count(); { int fds[3] = { 0, 1, 2 }, codes[3] = { 115, 115, 115 }, ms[3] = { 30, 30, 30 }; int rc = mp_keyq_submit(fds, codes, ms, 3, rec_sink); CHECK(rc == MP_KEYQ_OK, "批次(3 个同键 tap)作为一个作业入队"); int fd4 = 3, code4 = 114; rc = mp_keyq_submit(&fd4, &code4, (int[]){ 30 }, 1, rec_sink); CHECK(rc == MP_KEYQ_OK, "紧随其后的单个请求入队成功(不丢)"); } CHECK(wait_done(base, 2, 2000), "批次 + 单键在预算内执行完"); usleep(20 * 1000); CHECK(g_samp_edges == 4, "采样器看到 4 次按下(3 个同键 tap 未被合并)"); CHECK(g_rec_code[0] == 115 && g_rec_code[1] == 115 && g_rec_code[2] == 115 && g_rec_code[3] == 114, "批次内 3 次 115 顺序执行,之后才是下一个请求的 114"); printf("== 有界预算:超预算立即拒绝,不做无界等待 ==\n"); { int fd = 9, code = 115; int base2 = mp_keyq_done_count(); /* 每个作业实际时长 = press_ms + MP_KEYQ_GAP_MS(20),预算 2000ms */ int rc1 = mp_keyq_submit(&fd, &code, (int[]){ 800 }, 1, rec_sink); CHECK(rc1 == MP_KEYQ_OK, "800ms 作业入队(820ms 预算占用)"); int rc2 = mp_keyq_submit(&fd, &code, (int[]){ 800 }, 1, rec_sink); CHECK(rc2 == MP_KEYQ_OK, "第二个 800ms 作业入队(累计 1640ms)"); int rc3 = mp_keyq_submit(&fd, &code, (int[]){ 400 }, 1, rec_sink); CHECK(rc3 == MP_KEYQ_ERR_BUSY, "第三个作业超 2000ms 预算 → 立刻 MP_KEYQ_ERR_BUSY(503,不排队等)"); int rc4 = mp_keyq_submit(&fd, &code, (int[]){ 0 }, MP_KEYQ_JOB_MAX + 1, rec_sink); CHECK(rc4 == MP_KEYQ_ERR_BUSY, "作业超过 MP_KEYQ_JOB_MAX → 拒绝"); CHECK(wait_done(base2, 2, 4000), "两个 800ms 作业在预算内执行完"); int rc5 = mp_keyq_submit(&fd, &code, (int[]){ 80 }, 1, rec_sink); CHECK(rc5 == MP_KEYQ_OK, "预算释放后可继续提交(拒绝不是永久性的)"); } g_samp_stop = 1; pthread_join(st, NULL); printf(g_fail ? "\nRESULT: FAIL\n" : "\nRESULT: ALL PASS\n"); return g_fail ? 1 : 0; }