/* test_notif_panel.c — 宿主单测:阶段B 通知中心面板(纯逻辑 + 面板读取的存储契约)。 * * 直接 #include "meow_notif.c" + "meow_settings.c" 于同一 TU(同 test_notif_tf.c 手法), * 以「真实 meow_notif 存储 + 真实 meow_settings 后端」验证面板真正读取的字段契约; * 再测 meow_notif_panel.h 的纯逻辑(滑条映射/亮度 clamp/顶部下滑判定/时间显示/行命中/滚动钳位)。 * 不依赖 launcher 其它 static / fb / 触摸。 * * 运行:gcc -D_GNU_SOURCE -o test_notif_panel test_notif_panel.c && ./test_notif_panel */ #define _GNU_SOURCE #include #include #include #include #include #include /* stub:meow_notif.c 的 read_quota() 经 meow_settings_get_int 读 notify.* 配额。 * <0 => 键缺失 => 用默认(MEOW_NOTIF_CATS_DEF=4 / CAT_QUOTA_DEF=4 / MEDIA_QUOTA_DEF=1)。 * meow_notif.c 对 meow_settings 的唯一依赖就是这一个函数。 */ int meow_settings_get_int(const char *key, int *out) { (void)key; (void)out; return -2; /* MEOW_ERR_NOENT */ } /* 先把 tf 转储路径指向临时目录,避免污染真实卡路径(meow_notif_init 会 clear tf)*/ static char g_tflog[600]; #include "meow_notif.c" /* 真实通知存储:post/list/count/dismiss/update_progress/... */ #include "meow_notif_panel.h" /* 面板纯逻辑(header-only) */ static int g_fail = 0; #define CHECK(cond, msg) do { \ if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \ else fprintf(stderr, "ok: %s\n", msg); \ } while (0) /* 收集列表到固定槽(回调式,模拟面板 ncp_collect_cb 的字段读取) */ #define MAXG 16 typedef struct { uint32_t id; char summary[96]; char body[96]; char icon[64]; char app[48]; char tgt[48]; int progress; uint8_t urgency; uint64_t ms; } gslot_t; static gslot_t g_got[MAXG]; static int g_nslots = 0; static int list_grab_cb(const meow_notif_t *n, void *ctx) { (void)ctx; if (g_nslots >= MAXG) return 1; gslot_t *s = &g_got[g_nslots++]; memset(s, 0, sizeof *s); s->id = n->id; snprintf(s->summary, sizeof s->summary, "%s", n->summary); snprintf(s->body, sizeof s->body, "%s", n->body); snprintf(s->icon, sizeof s->icon, "%s", n->app_icon); snprintf(s->app, sizeof s->app, "%s", n->app_id); snprintf(s->tgt, sizeof s->tgt, "%s", n->target_activity); s->progress = n->progress; s->urgency = n->urgency; s->ms = n->created_ms; return 0; } static int grab_list(void) { g_nslots = 0; meow_notif_list(NULL, list_grab_cb, NULL); return g_nslots; } static gslot_t *find_slot(const char *summary) { for (int i = 0; i < g_nslots; i++) if (!strcmp(g_got[i].summary, summary)) return &g_got[i]; return NULL; } static uint64_t wall_now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; } static void test_pure_logic(void) { fprintf(stderr, "--- P1 pure logic ---\n"); /* mnp_top_swipe_down */ CHECK(mnp_top_swipe_down(40, 5, 3, 90, 28, 65) == 1, "top hotzone down-dominant -> open"); CHECK(mnp_top_swipe_down(10, 5, 3, 90, 28, 65) == 0, "left-edge (down_x<24) reserved -> no open"); CHECK(mnp_top_swipe_down(40, 30, 3, 90, 28, 65) == 0, "origin below hotzone -> no open"); CHECK(mnp_top_swipe_down(40, 5, 3, 40, 28, 65) == 0, "below swipe threshold -> no open"); CHECK(mnp_top_swipe_down(40, 5, 100, 70, 28, 65) == 0, "horizontal-dominant -> no open"); CHECK(mnp_top_swipe_down(40, 5, -3, 90, 28, 65) == 1, "down with minor left drift still opens"); /* mnp_slider_val_from_x */ CHECK(mnp_slider_val_from_x(20, 20, 440, 1, 100) == 1, "slider x=left -> min"); CHECK(mnp_slider_val_from_x(460, 20, 440, 1, 100) == 100, "slider x=right -> max"); int mid = mnp_slider_val_from_x(240, 20, 440, 1, 100); CHECK(mid >= 1 && mid <= 100, "slider x=mid within [min,max]"); CHECK(mnp_slider_val_from_x(0, 20, 440, 1, 100) == 1, "slider x out-left clamped to min"); CHECK(mnp_slider_val_from_x(999, 20, 440, 1, 100) == 100, "slider x out-right clamped to max"); /* mnp_step_round:跳步吸附(音量滑条去抖/减少逐值写 DAC) */ CHECK(mnp_step_round(37, 1) == 37, "step_round step=1 -> unchanged (continuous, same as settings slider)"); CHECK(mnp_step_round(0, 1) == 0, "step_round step=1 -> 0 kept"); CHECK(mnp_step_round(37, 5) == 35, "step_round 37 step=5 -> 35 (nearest multiple of 5)"); CHECK(mnp_step_round(38, 5) == 40, "step_round 38 step=5 -> 40 (rounds up to nearest)"); CHECK(mnp_step_round(50, 5) == 50, "step_round 50 step=5 -> exactly on step"); CHECK(mnp_step_round(0, 5) == 0, "step_round 0 step=5 -> 0"); CHECK(mnp_step_round(100, 5) == 100, "step_round 100 step=5 -> 100"); CHECK(mnp_step_round(99, 5) == 100, "step_round 99 step=5 -> 100 (rounds up to max)"); CHECK(mnp_step_round(2, 4) == 4, "step_round 2 step=4 -> 4 (round to nearest multiple)"); CHECK(mnp_step_round(-3, 5) == 0, "step_round negative clamps to 0 (value domain >=0)"); /* mnp_brightness_clamp */ CHECK(mnp_brightness_clamp(0, 1) == 1, "brightness clamp below min -> min"); CHECK(mnp_brightness_clamp(150, 1) == 100, "brightness clamp above 100 -> 100"); CHECK(mnp_brightness_clamp(50, 1) == 50, "brightness 50 kept"); CHECK(mnp_brightness_clamp(3, 5) == 5, "brightness below custom min -> custom min"); CHECK(mnp_brightness_clamp(-7, -3) == 1, "brightness clamp forces vmin>=1"); /* mnp_time_ago */ uint64_t now = wall_now_ms(); char tb[32]; mnp_time_ago(now - 5000, now, tb, sizeof tb); CHECK(!strcmp(tb, "刚刚"), "time_ago <60s -> 刚刚"); mnp_time_ago(now - 5 * 60000u, now, tb, sizeof tb); CHECK(!strcmp(tb, "5分钟前"), "time_ago 5min -> 5分钟前"); mnp_time_ago(now - 3 * 3600000u, now, tb, sizeof tb); CHECK(strlen(tb) == 5 && tb[2] == ':', "time_ago >1h -> HH:MM"); /* mnp_row_at */ CHECK(mnp_row_at(250, 252, 696, 92) == -1, "row_at above list -> -1"); CHECK(mnp_row_at(300, 252, 696, 92) == 0, "row_at first row -> 0"); CHECK(mnp_row_at(400, 252, 696, 92) == 1, "row_at second row -> 1"); CHECK(mnp_row_at(700, 252, 696, 92) == -1, "row_at below list -> -1"); /* mnp_scroll_clamp */ CHECK(mnp_scroll_clamp(0, 2000, 444) == 0, "scroll clamp lower -> 0"); CHECK(mnp_scroll_clamp(9999, 2000, 444) == 1556, "scroll clamp upper -> total-view"); CHECK(mnp_scroll_clamp(-5, 2000, 444) == 0, "scroll clamp negative -> 0"); CHECK(mnp_scroll_clamp(5, 400, 444) == 0, "content fits view -> 0"); } /* P4 通知列表滚动:方向映射(input.swipe_invert)+ 回弹判定(纯逻辑,header-only)。 * 语义与设置页列表一致:invert=0 手指上滑(d<0) → target 增大(内容上滚看后面项); * invert=1(反向)手指上滑 → target 减小。进滚动锁 base 保证触发瞬间不跳变。 */ static void test_scroll_pure(void) { fprintf(stderr, "--- P4 notification list scroll (swipe_invert direction + rebound) ---\n"); int total = 2000, view = 444, max_scroll = total - view; /* 1556 */ /* mnp_scroll_max */ CHECK(mnp_scroll_max(2000, 444) == 1556, "scroll_max overflow -> total-view"); CHECK(mnp_scroll_max(400, 444) == 0, "scroll_max content fits -> 0"); CHECK(mnp_scroll_max(444, 444) == 0, "scroll_max equal -> 0"); CHECK(mnp_scroll_max(445, 444) == 1, "scroll_max 1px over -> 1"); /* mnp_scroll_base:进滚动锁基准(invert=0 同设置页 `scroll + d`;invert=1 反向) */ CHECK(mnp_scroll_base(100, -45, 0) == 55, "base invert=0 finger-up -> scroll+d"); CHECK(mnp_scroll_base(100, -45, 1) == 145, "base invert=1 finger-up -> scroll-d"); /* mnp_scroll_target:触发瞬间 target 回到原 scroll(连续跟手,不跳变) */ CHECK(mnp_scroll_target(55, -45, 0) == 100, "target invert=0 at engage -> back to scroll"); CHECK(mnp_scroll_target(145, -45, 1) == 100, "target invert=1 at engage -> back to scroll"); /* 方向映射:invert=0 手指持续上滑(更负) → target 增大;invert=1 → target 减小 */ CHECK(mnp_scroll_target(55, -200, 0) == 255, "target invert=0 finger-up more -> increases"); CHECK(mnp_scroll_target(145, -200, 1) == -55, "target invert=1 finger-up more -> decreases"); CHECK(mnp_scroll_target(55, 200, 0) == -145, "target invert=0 finger-down -> decreases"); CHECK(mnp_scroll_target(145, 200, 1) == 345, "target invert=1 finger-down -> increases"); /* clamp 落位:进滚动 + 拖到边界 → 钳到 [0,max_scroll] */ CHECK(mnp_scroll_clamp(mnp_scroll_target(55, -200, 0), total, view) == 255, "clamp invert=0 up -> in range"); CHECK(mnp_scroll_clamp(mnp_scroll_target(145, -200, 1), total, view) == 0, "clamp invert=1 up -> bottom(0)"); CHECK(mnp_scroll_clamp(mnp_scroll_target(55, 200, 0), total, view) == 0, "clamp invert=0 down -> top(0)"); /* mnp_scroll_rebound:边内容差 tol=48;界内不触发,越过才触发 */ CHECK(mnp_scroll_rebound(0, max_scroll, 48) == 0, "rebound at top(0) in-tol -> 0"); CHECK(mnp_scroll_rebound(-47, max_scroll, 48) == 0, "rebound top overshoot 47<48 -> 0"); CHECK(mnp_scroll_rebound(-49, max_scroll, 48) == 1, "rebound top overshoot 49>48 -> 1"); CHECK(mnp_scroll_rebound(max_scroll, max_scroll, 48) == 0, "rebound at bottom in-tol -> 0"); CHECK(mnp_scroll_rebound(max_scroll + 47, max_scroll, 48) == 0, "rebound bottom overshoot 47<48 -> 0"); CHECK(mnp_scroll_rebound(max_scroll + 49, max_scroll, 48) == 1, "rebound bottom overshoot 49>48 -> 1"); CHECK(mnp_scroll_rebound(500, max_scroll, 48) == 0, "rebound mid-range -> 0"); } /* P3 开关平移动画状态机:p 推进/翻转/中断、offset 计算、裁切边界、关闭到 0 才退出。 * 用任意 fix/ph 参数化(默认 1024/692,通知中心面板 PH=720-28=692);纯函数不依赖 fb。 */ static void test_anim_pure(void) { fprintf(stderr, "--- P3 open/close slide animation state machine ---\n"); enum { FIX = 1024, PH = 692 }; /* mnp_anim_off:p→offset 线性映射(∈[-PH,0];p=0 全隐藏 / p=FIX 到位) */ CHECK(mnp_anim_off(0, FIX, PH) == -PH, "anim_off p=0 -> -PH (fully hidden above)"); CHECK(mnp_anim_off(FIX, FIX, PH) == 0, "anim_off p=FIX -> 0 (settled open)"); CHECK(mnp_anim_off(FIX / 2, FIX, PH) == -(PH / 2), "anim_off mid -> -PH/2"); /* mnp_anim_step:打开推进(0→FIX) */ CHECK(mnp_anim_step(0, FIX, FIX, 180, 180) == FIX, "step dt=dur -> reaches FIX in one tick"); CHECK(mnp_anim_step(0, FIX, FIX, 90, 180) == FIX / 2, "step dt=dur/2 -> half progress"); CHECK(mnp_anim_step(0, FIX, FIX, 10, 180) >= 1, "step small dt -> at least 1 (monotonic, no stall)"); CHECK(mnp_anim_step(0, FIX, FIX, 0, 180) == 0, "step dt=0 -> no move"); CHECK(mnp_anim_step(0, FIX, FIX, 100000, 180) == FIX, "step huge dt -> clamped at FIX"); /* 反向推进(关闭:FIX→0,线性步进率 = dt*fix/dur,非剩余比例) */ CHECK(mnp_anim_step(FIX, 0, FIX, 90, 180) == FIX / 2, "step close dt=dur/2 -> half back"); CHECK(mnp_anim_step(FIX / 2, 0, FIX, 45, 180) == FIX / 4, "step reversal mid -> toward 0 linearly"); CHECK(mnp_anim_step(FIX / 4, 0, FIX, 100000, 180) == 0, "step reversal huge dt -> clamped at 0"); /* 打断(打开中→关闭→再打开):平滑转向,不跳变到目标 */ int p = mnp_anim_step(0, FIX, FIX, 90, 180); /* 打开到半程 (512) */ int p_before = p; p = mnp_anim_step(p, 0, FIX, 30, 180); /* 打断:转为关闭,小步推进 */ CHECK(p < p_before && p > 0, "interrupt open->close: heading to 0 (not target), mid value ok"); int s2 = 0; while (s2++ < 1000 && p != 0) p = mnp_anim_step(p, 0, FIX, 30, 180); CHECK(p == 0, "interrupt close completes at 0"); p = mnp_anim_step(p, FIX, FIX, 180, 180); /* 关闭到位后再打开 */ CHECK(p == FIX, "reopen after interrupt -> settled open"); /* mnp_anim_covered_bot:裁切边界(off∈[-PH,0] → bot∈[top, top+PH]) */ CHECK(mnp_anim_covered_bot(28, PH, -PH) == 28, "covered_bot fully hidden -> top (statusbar bottom)"); CHECK(mnp_anim_covered_bot(28, PH, 0) == 28 + PH, "covered_bot settled -> top+PH"); CHECK(mnp_anim_covered_bot(28, PH, -(PH / 2)) == 28 + PH / 2, "covered_bot halfway -> top+PH/2"); CHECK(mnp_anim_covered_bot(28, PH, 0) <= 720 && mnp_anim_covered_bot(28, PH, -PH) >= 28, "covered_bot within [28,720] — 不盖状态栏、不出屏底"); /* 完整关闭:多次 tick 才到 0;单调不增;且只在 p==0 才算退出 */ p = FIX; int steps = 0, prev = p, monotonic = 1; while (steps++ < 100000 && p != 0) { int np = mnp_anim_step(p, 0, FIX, 10, 180); if (np > prev) monotonic = 0; /* 关闭方向必须单调减小 */ if (np < 0 || np > FIX) monotonic = 0; p = np; prev = np; } CHECK(p == 0, "close animation terminates at p=0 (exit condition)"); CHECK(steps > 1, "close takes multiple ticks (smooth sliding, not a jump)"); CHECK(monotonic, "close progress monotonic non-increasing"); CHECK(mnp_anim_step(0, 0, FIX, 10, 180) == 0, "at p==0 with target 0 stays put (would break)"); } /* P5 ⑥ 勿扰过滤 + ⑦ 行左滑删除(纯逻辑,header-only): * - mnp_dnd_should_show:dnd=0 全显示;dnd=1 只 urgency>=2(HIGH/CRITICAL)。 * - mnp_swipe_clamp:跟手 clamp 到 [-row_w,0]。 * - mnp_swipe_delete:松手阈值(行宽百分比 / 绝对像素,取大者)。 * - mnp_swipe_step:落位动画线性步进(回弹/滑出,正反向,到底停,半程取消)。 * 另加一段 mini 状态机:手指拖 → 松手过阈值=滑出删除 / 未过=回弹复原(跟手、半程可回)。 */ static void test_dnd_swipe_pure(void) { fprintf(stderr, "--- P5 DND filter + row left-swipe ---\n"); /* mnp_dnd_should_show */ CHECK(mnp_dnd_should_show(0, 0) == 1, "dnd off -> LOW(0) shows"); CHECK(mnp_dnd_should_show(1, 0) == 1, "dnd off -> NORMAL(1) shows"); CHECK(mnp_dnd_should_show(2, 0) == 1, "dnd off -> HIGH(2) shows"); CHECK(mnp_dnd_should_show(3, 0) == 1, "dnd off -> CRITICAL(3) shows"); CHECK(mnp_dnd_should_show(0, 1) == 0, "dnd on -> LOW(0) hidden"); CHECK(mnp_dnd_should_show(1, 1) == 0, "dnd on -> NORMAL(1) hidden"); CHECK(mnp_dnd_should_show(2, 1) == 1, "dnd on -> HIGH(2) shows"); CHECK(mnp_dnd_should_show(3, 1) == 1, "dnd on -> CRITICAL(3) shows"); /* mnp_swipe_clamp(跟手 offset clamp:off∈[-row_w,0]) */ CHECK(mnp_swipe_clamp(-200, 456) == -200, "swipe_clamp in-range kept"); CHECK(mnp_swipe_clamp(30, 456) == 0, "swipe_clamp rightward -> 0"); CHECK(mnp_swipe_clamp(-500, 456) == -456, "swipe_clamp past left -> -row_w"); CHECK(mnp_swipe_clamp(-456, 456) == -456, "swipe_clamp exactly -row_w kept"); CHECK(mnp_swipe_clamp(0, 456) == 0, "swipe_clamp 0 kept"); /* mnp_swipe_delete(row_w=456, frac_pct=40 → 40%=182.4;min_px=110;OR 语义 → 有效阈值=110px) */ CHECK(mnp_swipe_delete(-80, 456, 40, 110) == 0, "delete -80 (<110 & <182.4) -> cancel"); CHECK(mnp_swipe_delete(-110, 456, 40, 110) == 1, "delete -110 (>=min_px) -> delete"); CHECK(mnp_swipe_delete(-150, 456, 40, 110) == 1, "delete -150 (>=min_px, <40%) -> delete"); CHECK(mnp_swipe_delete(-183, 456, 40, 110) == 1, "delete -183 (>=40%) -> delete"); CHECK(mnp_swipe_delete(-456, 456, 40, 110) == 1, "delete full slide -> delete"); CHECK(mnp_swipe_delete(0, 456, 40, 110) == 0, "delete 0 -> cancel"); /* 窄行:40%=24px(row_w=60),min_px=110 不起作用 → 阈值=24px */ CHECK(mnp_swipe_delete(-25, 60, 40, 110) == 1, "delete narrow row -25 (>=40%) -> delete"); CHECK(mnp_swipe_delete(-20, 60, 40, 110) == 0, "delete narrow row -20 (<40%) -> cancel"); /* mnp_swipe_step(线性步进:off 向 target 走 dt*rate,可正/反向,到底停) */ CHECK(mnp_swipe_step(-100, 0, 10, 3) == -70, "step springback +30px/tick"); CHECK(mnp_swipe_step(-100, 0, 0, 3) == -100, "step dt=0 -> unchanged"); CHECK(mnp_swipe_step(-100, 0, 1000, 3) == 0, "step huge dt -> clamp at 0 (springback done)"); CHECK(mnp_swipe_step(-50, -456, 10, 3) == -80, "step slide-out leftward"); CHECK(mnp_swipe_step(-50, -456, 1000, 3) == -456, "step huge dt -> clamp at -row_w (deleted)"); CHECK(mnp_swipe_step(-30, -30, 10, 3) == -30, "step at target -> stays"); CHECK(mnp_swipe_step(0, 0, 10, 3) == 0, "step 0 target 0 -> stays"); CHECK(mnp_swipe_step(-1000, 0, 5, 3) == -985, "step linear rate not exponential (small dt)"); /* mini 状态机:跟手拖 → 松手判定 → 动画到目标(半程取消 vs 删除)。 */ int row_w = 456, off = 0; /* 拖 80px 左(低于阈值 110)→ 松手回弹复原 */ off = mnp_swipe_clamp(-80, row_w); CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: drag -80 -> cancel (springback)"); int target = mnp_swipe_delete(off, row_w, 40, 110) ? -row_w : 0; off = mnp_swipe_step(off, target, 1000, 3); CHECK(off == 0, "mini: springback reaches 0 (row restored)"); /* 拖 150px 左(>=110)→ 松手滑出删除 */ off = mnp_swipe_clamp(-150, row_w); CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 1, "mini: drag -150 -> delete"); target = mnp_swipe_delete(off, row_w, 40, 110) ? -row_w : 0; off = mnp_swipe_step(off, target, 1000, 3); CHECK(off == -row_w, "mini: slide-out reaches -row_w (dismiss)"); /* 半程可反向:拖 200 左再反向回摄(dx 变 +30)→ off 回 0(跟手可回、松手在途取消) */ off = mnp_swipe_clamp(-200, row_w); CHECK(off == -200, "mini: drag -200 follows finger"); off = mnp_swipe_clamp(30, row_w); /* 反向拖回原点左侧以右 → clamp 0 */ CHECK(off == 0, "mini: reverse drag -> row slides back to 0 (halfway cancel)"); /* 半程取消:拖 150 左(未达 182.4 的 40% 但达 110px 阈值)→ 松手删;若再拖回 <110 → 回弹 */ off = mnp_swipe_clamp(-150, row_w); CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 1, "mini: -150 passed threshold -> delete"); off = mnp_swipe_clamp(-100, row_w); /* 又拖回 100 <110 */ CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: -100 back under threshold -> cancel"); } /* P6 ⑧ 一键清除判定 + ⑨ 面板跟手下拉/半程可回(纯逻辑,header-only): * - mnp_clearable:红线(ongoing / media / progress>=0 进行中)一律保留;非活动普通通知可清。 * - mnp_drag_progress:reveal→progress(0/fix/线性/越界 clamp)。 * - mnp_drag_reveal:seed+delta 正向/反向/边界 clamp(连续、半程可回)。 * - mnp_drag_anchor:松手锚定(≥40%→前去 fix;<40%→回 0)。 * 另加 mini 状态机:开(跟手下拉→松手过阈值展开/未过收回)+ 关(上滑跟手→松手未过阈值回弹重展/过阈值收回)。 */ static void test_clear_drag_pure(void) { fprintf(stderr, "--- P6 clear predicate + panel drag-follow ---\n"); enum { FIX = 1024, PH = 692 }; /* mnp_clearable */ CHECK(mnp_clearable(1, 0, -1) == 0, "clearable: ongoing -> keep"); CHECK(mnp_clearable(0, 1, -1) == 0, "clearable: media -> keep"); CHECK(mnp_clearable(0, 0, 50) == 0, "clearable: progress in-progress(50) -> keep"); CHECK(mnp_clearable(0, 0, 0) == 0, "clearable: progress bar(0) -> keep"); CHECK(mnp_clearable(0, 0, -1) == 1, "clearable: plain inactive -> clear"); CHECK(mnp_clearable(0, 1, 50) == 0, "clearable: media+progress -> keep"); CHECK(mnp_clearable(1, 0, 50) == 0, "clearable: ongoing+progress -> keep"); /* mnp_drag_progress(reveal∈[0,ph] → p∈[0,fix]) */ CHECK(mnp_drag_progress(0, FIX, PH) == 0, "drag_progress reveal=0 -> 0"); CHECK(mnp_drag_progress(PH, FIX, PH) == FIX, "drag_progress reveal=ph -> fix"); CHECK(mnp_drag_progress(PH / 2, FIX, PH) == FIX / 2, "drag_progress mid -> half"); CHECK(mnp_drag_progress(-50, FIX, PH) == 0, "drag_progress negative reveal -> 0"); CHECK(mnp_drag_progress(PH + 50, FIX, PH) == FIX, "drag_progress reveal>ph -> fix"); CHECK(mnp_drag_progress(173, FIX, 692) == FIX * 173 / 692, "drag_progress linear 173/692"); /* mnp_drag_reveal(seed+delta → [0,ph],连续/反向/边界) */ CHECK(mnp_drag_reveal(0, 0, PH) == 0, "drag_reveal seed0 delta0 -> 0"); CHECK(mnp_drag_reveal(0, 300, PH) == 300, "drag_reveal open: seed0+300 -> 300 (1:1)"); CHECK(mnp_drag_reveal(PH, -300, PH) == PH - 300, "drag_reveal close: seed ph -300"); CHECK(mnp_drag_reveal(100, -200, PH) == 0, "drag_reveal clamps below 0 (reverse past top)"); CHECK(mnp_drag_reveal(100, 1000, PH) == PH, "drag_reveal clamps above ph (drag past bottom)"); CHECK(mnp_drag_reveal(0, -50, PH) == 0, "drag_reveal reverse on hidden stays 0"); CHECK(mnp_drag_reveal(50, 0, 0) == 0, "drag_reveal ph<=0 -> 0"); /* mnp_drag_anchor(≥fix*40/100=409 → 1;<409 → 0) */ CHECK(mnp_drag_anchor(409, FIX, 40) == 1, "drag_anchor at 40% (409) -> go open"); CHECK(mnp_drag_anchor(410, FIX, 40) == 1, "drag_anchor above 40% -> go open"); CHECK(mnp_drag_anchor(1024, FIX, 40) == 1, "drag_anchor full -> go open"); CHECK(mnp_drag_anchor(408, FIX, 40) == 0, "drag_anchor below 40% -> collapse"); CHECK(mnp_drag_anchor(0, FIX, 40) == 0, "drag_anchor 0 -> collapse"); CHECK(mnp_drag_anchor(0, FIX, 0) == 1, "drag_anchor thr_pct=0 -> always open"); /* mini 状态机:跟手下拉打开(seed 从0 升)→ 松手锚定 */ int seed = 0, p = 0; int dy = 0; /* 手指下移 300px:reveal=seed+(tly-drag_y0) */ seed = 0; p = mnp_drag_progress(mnp_drag_reveal(seed, 300, PH), FIX, PH); CHECK(p > 0 && p < FIX, "mini open-follow: drag 300px -> partial progress"); CHECK(mnp_drag_anchor(p, FIX, 40) == 1, "mini open-follow: 300/692=43% >= 40% -> open"); /* 反向拖回 200(reveal=200,即拖 200)→ 半程取消(29% < 40%) */ p = mnp_drag_progress(mnp_drag_reveal(seed, 200, PH), FIX, PH); CHECK(p < FIX && mnp_drag_anchor(p, FIX, 40) == 0, "mini open-follow: 200/692=29% < 40% -> collapse(cancel)"); /* 全拉满 → 满 */ CHECK(mnp_drag_anchor(mnp_drag_progress(mnp_drag_reveal(seed, PH, PH), FIX, PH), FIX, 40) == 1, "mini open-follow: drag full -> open"); /* 关闭:seed=PH 上滑 600 → reveal=PH-600 → 过阈值收回 */ int close_p = mnp_drag_progress(mnp_drag_reveal(PH, -600, PH), FIX, PH); CHECK(close_p < FIX && mnp_drag_anchor(close_p, FIX, 40) == 0, "mini close-follow: up 600 -> close"); /* 关闭:上滑仅 150(reveal=PH-150,仍高)→ 松手回弹重展 */ int reopen_p = mnp_drag_progress(mnp_drag_reveal(PH, -150, PH), FIX, PH); CHECK(reopen_p > 0 && mnp_drag_anchor(reopen_p, FIX, 40) == 1, "mini close-follow: up 150 -> reopen"); } /* P7 ⑧ 一键清除(存储契约):先用 mnp_clearable + meow_notif_list/dismiss 模拟面板 ncp_clear_all, * 验证「先转储留档(dismiss 触发 dump_cb)→ 清出显示」且红线项(ongoing/media/progress)保留。 */ typedef struct { uint32_t ids[8]; int n; } clear_ctx_t; static int clear_collect_cb(const meow_notif_t *n, void *ctx) { clear_ctx_t *c = ctx; if (!mnp_clearable(n->ongoing, n->media, n->progress)) return 0; if (c->n >= 8) return 1; c->ids[c->n++] = n->id; return 0; } /* 通用「收集全部活跃 id」(兼作测试排空用:非仅可清)。 */ static int clear_all_cb(const meow_notif_t *n, void *ctx) { clear_ctx_t *c = ctx; if (c->n >= 8) return 1; c->ids[c->n++] = n->id; return 0; } static void test_clear_storage(void) { fprintf(stderr, "--- P7 clear-all storage contract (dump then clear, keep ongo/media/progress) ---\n"); /* 排空上一测试留下的所有活跃通知,保证本测试从干净基数开始(count 断言可用绝对值)。 */ for (int pass = 0; pass < 8 && meow_notif_count(NULL) > 0; pass++) { clear_ctx_t c2; memset(&c2, 0, sizeof c2); meow_notif_list(NULL, clear_all_cb, &c2); /* clear_all_cb:收集全部(非仅可清) */ if (c2.n == 0) break; for (int i = 0; i < c2.n; i++) meow_notif_dismiss(c2.ids[i]); } meow_notif_t n; memset(&n, 0, sizeof n); /* 普通(可清) */ snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general"); snprintf(n.summary, sizeof n.summary, "普通1"); n.progress = -1; n.urgency = 1; uint32_t idA = 0; CHECK(meow_notif_post(&n, &idA) == NOTIF_OK, "clear st: post plain1"); /* 普通2(可清) */ memset(&n, 0, sizeof n); snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general"); snprintf(n.summary, sizeof n.summary, "普通2"); n.progress = -1; n.urgency = 1; uint32_t idB = 0; CHECK(meow_notif_post(&n, &idB) == NOTIF_OK, "clear st: post plain2"); /* 媒体(红线:保留) */ memset(&n, 0, sizeof n); snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "media"); snprintf(n.summary, sizeof n.summary, "媒体"); n.ongoing = 1; n.media = 1; n.progress = 30; n.urgency = 2; uint32_t idM = 0; CHECK(meow_notif_post(&n, &idM) == NOTIF_OK, "clear st: post media"); /* 进度进行中(红线:保留) */ memset(&n, 0, sizeof n); snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general"); snprintf(n.summary, sizeof n.summary, "下载中"); n.progress = 60; n.urgency = 2; uint32_t idP = 0; CHECK(meow_notif_post(&n, &idP) == NOTIF_OK, "clear st: post progress"); CHECK(meow_notif_count(NULL) == 4, "clear st: count 4 before clear"); /* 按 mnp_clearable 收集 + dismiss(= dump_cb + 释放槽,模拟 ncp_clear_all) */ clear_ctx_t c; memset(&c, 0, sizeof c); meow_notif_list(NULL, clear_collect_cb, &c); int cleared = 0; for (int i = 0; i < c.n; i++) { meow_notif_dismiss(c.ids[i]); cleared++; } CHECK(cleared == 2, "clear st: exactly 2 plain cleared"); CHECK(meow_notif_count(NULL) == 2, "clear st: 2 remaining after clear (media+progress kept)"); grab_list(); CHECK(find_slot("媒体") != NULL, "clear st: media kept"); CHECK(find_slot("下载中") != NULL, "clear st: progress kept"); CHECK(find_slot("普通1") == NULL && find_slot("普通2") == NULL, "clear st: plain dismissed"); } static void test_storage_contract(void) { fprintf(stderr, "--- P0 storage contract (panel reads) ---\n"); /* meow_notif_init:应清内存表 + 清 tf 文件(路径已指临时);配额用默认(stub 返回 NOENT) */ CHECK(meow_notif_init() == NOTIF_OK, "meow_notif_init ok"); CHECK(meow_notif_count(NULL) == 0, "init clears memory table"); CHECK(g_max_cats == 4 && g_cat_quota == 4 && g_media_quota == 1, "quota defaults from meow_settings backend (stub NOENT -> defaults)"); meow_notif_t n; memset(&n, 0, sizeof n); snprintf(n.app_id, sizeof n.app_id, "os.meow.hello"); snprintf(n.category, sizeof n.category, "general"); snprintf(n.summary, sizeof n.summary, "你好世界"); snprintf(n.body, sizeof n.body, "这是正文第二行"); snprintf(n.app_icon, sizeof n.app_icon, "apps/os.meow.hello/icon.png"); snprintf(n.target_activity, sizeof n.target_activity, "activity_main"); n.expire_timeout = 0; n.progress = -1; n.urgency = 1; uint32_t id1 = 0; CHECK(meow_notif_post(&n, &id1) == NOTIF_OK, "post general ok"); CHECK(id1 != 0, "post assigns id"); /* 媒体通知:ongoing + 进度 */ memset(&n, 0, sizeof n); snprintf(n.app_id, sizeof n.app_id, "os.meow.hello"); snprintf(n.category, sizeof n.category, "media"); snprintf(n.summary, sizeof n.summary, "正在播放"); n.expire_timeout = 0xFFFFFFFFu; n.ongoing = 1; n.progress = 20; n.urgency = 2; uint32_t id2 = 0; CHECK(meow_notif_post(&n, &id2) == NOTIF_OK, "post media ok"); /* 高紧急 + 进度 */ memset(&n, 0, sizeof n); snprintf(n.app_id, sizeof n.app_id, "system"); snprintf(n.category, sizeof n.category, "event"); snprintf(n.summary, sizeof n.summary, "系统提醒"); snprintf(n.body, sizeof n.body, "电量低"); n.progress = 50; n.urgency = 3; uint32_t id3 = 0; CHECK(meow_notif_post(&n, &id3) == NOTIF_OK, "post event(high) ok"); CHECK(meow_notif_count(NULL) == 3, "count == 3 after posts"); /* 遍历(面板 ncp_collect_cb 同款字段读取)*/ int got = grab_list(); CHECK(got == 3, "list returns 3 active notifs"); gslot_t *s1 = find_slot("你好世界"); CHECK(s1 != NULL, "panel sees summary 你好世界"); if (s1) { CHECK(!strcmp(s1->app, "os.meow.hello"), "app_id"); CHECK(!strcmp(s1->body, "这是正文第二行"), "body"); CHECK(!strcmp(s1->icon, "apps/os.meow.hello/icon.png"), "app_icon"); CHECK(!strcmp(s1->tgt, "activity_main"), "target_activity"); CHECK(s1->progress == -1, "general progress=-1 (none)"); CHECK(s1->urgency == 1, "general urgency=NORMAL(1)"); CHECK(s1->ms > 0, "created_ms set"); } gslot_t *s2 = find_slot("正在播放"); CHECK(s2 != NULL && s2->progress == 20 && s2->urgency == 2, "media progress=20 urgency=HIGH(2)"); gslot_t *s3 = find_slot("系统提醒"); CHECK(s3 != NULL && s3->progress == 50 && s3->urgency == 3, "event progress=50 urgency=CRITICAL(3)"); /* dismiss → 面板列表减一(点通知=阅读)*/ CHECK(meow_notif_dismiss(id1) == NOTIF_OK, "dismiss general ok"); CHECK(meow_notif_count(NULL) == 2, "count == 2 after dismiss"); grab_list(); CHECK(find_slot("你好世界") == NULL, "dismissed notif no longer listed"); /* 里程碑 100% → 释放(非 ONGOING):面板列表再减一 */ CHECK(meow_notif_update_progress(id3, 100) == NOTIF_OK, "update_progress(100) ok"); CHECK(meow_notif_count(NULL) == 1, "milestone released event slot -> count 1"); grab_list(); CHECK(find_slot("系统提醒") == NULL, "100% milestone notif no longer listed"); /* 剩余媒体通知仍活跃(ONGOING 常驻)*/ CHECK(find_slot("正在播放") != NULL, "ongoing media stays listed"); /* 配额:单 app 媒体单例(media_quota=1)——再发一条 media 顶掉旧媒体 */ memset(&n, 0, sizeof n); snprintf(n.app_id, sizeof n.app_id, "os.meow.hello"); snprintf(n.category, sizeof n.category, "media"); snprintf(n.summary, sizeof n.summary, "下一首"); n.expire_timeout = 0xFFFFFFFFu; n.ongoing = 1; n.progress = 60; uint32_t id4 = 0; CHECK(meow_notif_post(&n, &id4) == NOTIF_OK, "post second media ok"); CHECK(meow_notif_count(NULL) == 1, "media quota=1 -> old media replaced, count stays 1"); grab_list(); CHECK(find_slot("下一首") != NULL && find_slot("正在播放") == NULL, "media singleton replaced oldest"); } int main(void) { char tmp[] = "/tmp/meow_notif_panel_XXXXXX"; char *dir = mkdtemp(tmp); if (!dir) { perror("mkdtemp"); return 2; } snprintf(g_tflog, sizeof g_tflog, "%s/notif.log", dir); meow_notif_set_tf_path(g_tflog); meow_notif_set_tf_max(1024 * 1024); test_pure_logic(); test_scroll_pure(); test_anim_pure(); test_dnd_swipe_pure(); test_clear_drag_pure(); test_storage_contract(); test_clear_storage(); unlink(g_tflog); rmdir(dir); fprintf(stderr, "\n%s (failures=%d)\n", g_fail ? "== TEST FAILED ==" : "== TEST PASSED ==", g_fail); return g_fail ? 1 : 0; }