/* 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_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"); } 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_storage_contract(); 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; }