Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_bt_page.c
T
AskaEth f49585dd62 fix(launcher): M47 r1 评审 P2 四项 — 未知态不写盘/不顶替 + deadline 从点击起算 + 开关弹窗后置清理
评审 r1 status=p2-4items / merge=fix-then-merge(reviewed e4eda3a)。逐条:

P2-1 未知态被写成 settings=0,把失真推到别的入口
- bt_page.c / wifi_page.c:`meow_settings_set_bool(..., disp == 1)` 加 `disp >= 0` 门。
  disp == -1(daemon 与直连都读不到)时**保留用户原意图不动**。该键被 notif_center /
  settings_page 当显示态读、被 app_launcher 用来决定自动拉起 WiFi,写 0 会把「不知道」
  污染成「确定已关」。
- test_bt_page.c 5d 期望同步改:未知 → settings 保持原意图 1(不再期望「回滚为 0」)。

P2-2 收尾后「探测未知」仍显示成确定态,第三态丢失
- mtog_calc:显示态**只认 probe**,probe < 0 直接返回 -1;last_real 不再顶替确定态
  (`(void)last_real`,仅由调用方用于弹窗文案)。超时/下发失败且探测未知 → 顶栏保留
  「关+?」、行内保留「状态未知」,与文件头契约一致。
- 弹窗文案按结论区分:「无法确认当前状态(上次已知:开启/关闭/未知)」vs
  「切换未生效:当前仍为开启/关闭」——不再出现「当前仍为开启」自相矛盾(该文案只在
  probe 已知且 != 意图时出现)。日志加 elapsed。

P2-3 deadline 起点错,注释上限是假的
- bt_page/wifi_page:`deadline` 从**点击时刻**起算(含下发耗时)。
- 注释改成如实上限:快路径 1~2s / ≈16 次探测;慢路径(on-off 读超时 1.5s + hciconfig
  0.3~2s,之后一次探测 status 1.5s + 直连 1.8s ≈3.3s)总阻塞最坏 ≈7s、预算内只够 1 次
  探测,此时只能确认「探测超时」而非「设备无响应」——文案已按此区分。异步化留给后续单。

P2-4 开关路径弹窗后缺 after_modal / 按键基线复位
- subpage.c:顶开关命中分支补上与行命中分支相同的后置清理(排空触摸 + 40ms 二次排空、
  触摸基线/cooldown、key_router_rebase、kd_*/kp_poll 基线、m_prev_*、after_modal=1)。
  s_dd_modal 是全屏压暗层,否则关闭后状态栏(0..27)残留压暗到下一次时钟重绘,电源/播放键
  也可能带进弹窗里的陈旧边沿。

顺带(评审观察,未改全局超时策略)
- netmgr_ipc.c:拆出 `nm_cmd_t(..., int *read_timeout)`;`nm_bt_power_state()` 走它,
  **读超时 = 状态未知且不把可达性缓存打成 down**(netmgrd 单线程忙时 `bt status` 自己
  跑 hciconfig 可能超 1.5s;旧行为会标 down 1s,令紧随的 bt_rows 闪出「服务未运行(直连
  模式)」并把后续探测切到 1.8s 直连)。WiFi/DNS 等其它调用保持旧语义(传 NULL)。

验证
- `bash patch_v2/meow_launcherd/build_launcher.sh test` → exit 0,**65 PASS / 0 FAIL / 0 warning**
  (矩阵 13 组,新增「下发失败·探测未知 → 未知不顶替」)。
- 交叉编译 0 warning;产物(临时副本,不入库)901020 B / md5 e21450ff6621a67fd854165413c58be1。
- `test_time32_abi.sh` → 4263=3 / 4403=0 ALL PASS;`pkg_install` 与 7 条切换中/未知态文案
  均在 strings 中(旧的两条「无法确认蓝牙/WiFi 状态」已合并为「无法确认当前状态(上次已知:%s)」)。
- 真机 --sysdump(只读,临时文件已删):exit 0,bt_on=1/wifi_on=1 与设备现状一致;
  Music du 前后 199164544;未替换 /usr/bin/meow_launcherd、未重启 launcherd。
- 交付形态不变:本 commit 只动 5 个源码文件;二进制与 scripts/build/fw_content.conf 对 main 零 diff。
2026-11-11 17:59:13 +08:00

361 lines
18 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_bt_page.c — MeowOS launcher 蓝牙子页(bt_page.c)sub_page_t 逻辑宿主自测(非生产代码)。
*
* 通过 #include bt_page.c 把其 FILE-STATIC 的 bt_top_sw/bt_rows/bt_act/bt_detail 平移进本 TU,
* 桩掉 netmgr IPC 客户端(nm_bt_*)+ 设置后端(meow_settings_*)+ UI(s_dd_modal/logmsg 等),
* 直接验证:
* P1 bt_rows 行骨架(plan §六 8 分组:②A2DP 摘要 ③输出目标 ④已连接 ⑤已配对 ⑥附近 ⑦音质 ⑧功耗)
* 与「已连接设备」数据(权威 netmgrd 状态 → mac/name/codec 渲染);
* P2 bt_act 命中 → 正确下发 netmgrd 命令(已连接→disconnect、已配对未连→connect、输出目标
* 切换、编解码器循环);
* P3 附近设备仅列未配对(已配对者归「已配对设备」)。
* P4 M47 开关三态:真实态未知(-1) → top_sw 回 -1 + 行内「蓝牙状态未知」(不当已关);
* P5 M47 点击收敛:下发成功且真实态达成 → 定稿、不弹窗;下发失败/超时/未知 → 回滚 + 弹窗;
* P6 M47 收敛矩阵(纯函数 meow_toggle_*,真实态×下发结果×探测 全组合)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -I../sdk -I. -Wall -Wextra -fsanitize=undefined test_bt_page.c -o test_bt_page
* 运行:./test_bt_page(或 `bash build_launcher.sh test`)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <strings.h>
#include <unistd.h>
#include <time.h>
/* M47:把收敛等待改成宿主可接受的尺度(生产 4000ms/250ms,见 bt_page.c)。 */
#define BT_TOGGLE_SETTLE_MS 80
#define BT_TOGGLE_POLL_MS 10
#include "meow_launcher_internal.h"
#include "bt_page.c" /* bt_rows/bt_act/bt_top_sw/bt_detail 平移到本 TU */
/* ---------- 测试态:netmgrd BT 状态桩(launcher 客户端视角) ---------- */
static meow_net_t g_conn[8], g_paired[8], g_near[8];
static int g_conn_n, g_paired_n, g_near_n;
static bt_ep_t g_eps[8]; static int g_eps_n;
static char g_last_cmd[128]; /* 记录 act 下发命令,如 "disconnect:<mac>" */
static int g_output_bt; /* audio.output_bt 桩值 */
static char g_codec[16] = "auto"; /* bt.codec 桩值 */
static int g_bt_state = 1; /* M47:真实态三态桩(-1 未知 / 0 / 1) */
static int g_onoff_rc = MEOW_OK; /* M47:nm_bt_onoff 下发结果桩 */
static int g_bt_en_setting = 1; /* M47:network.bluetooth_enabled 桩值(意图/回滚观察点) */
int nm_bt_power_state(void) { return g_bt_state; }
int nm_bt_onoff(int on) { (void)on; return g_onoff_rc; }
int nm_bt_connected(meow_net_t *out, int max) {
if (max > g_conn_n) max = g_conn_n;
memcpy(out, g_conn, (size_t)max * sizeof *out); return max;
}
int nm_bt_paired(meow_net_t *out, int max) {
if (max > g_paired_n) max = g_paired_n;
memcpy(out, g_paired, (size_t)max * sizeof *out); return max;
}
int nm_bt_devices(meow_net_t *out, int max) {
if (max > g_near_n) max = g_near_n;
memcpy(out, g_near, (size_t)max * sizeof *out); return max;
}
int nm_bt_a2dp(bt_ep_t *eps, int max) {
if (max > g_eps_n) max = g_eps_n;
memcpy(eps, g_eps, (size_t)max * sizeof *eps); return max;
}
static void set_cmd(const char *cmd, const char *mac)
{
snprintf(g_last_cmd, sizeof g_last_cmd, "%s:%s", cmd, mac ? mac : "");
}
int nm_bt_connect(const char *mac) { set_cmd("connect", mac); return MEOW_OK; }
int nm_bt_disconnect(const char *mac) { set_cmd("disconnect", mac); return MEOW_OK; }
int nm_bt_pair(const char *mac) { set_cmd("pair", mac); return MEOW_OK; }
int nm_bt_unpair(const char *mac) { set_cmd("unpair", mac); return MEOW_OK; }
void nm_bt_scan_on(void) {}
void nm_bt_scan_off(void) {}
int netmgr_ok(void) { return 1; }
void net_bt_mode_str(char *buf, int len) { snprintf(buf, (size_t)len, "主模式·已连 Pudding"); }
/* ---------- 设置/UI 桩 ---------- */
int meow_settings_get_bool(const char *k, int *v)
{
if (v) *v = 0;
if (!strcmp(k, "network.bluetooth_enabled")) { if (v) *v = g_bt_en_setting; return 0; }
if (!strcmp(k, "audio.output_bt")) { if (v) *v = g_output_bt; return 0; }
return -1;
}
int meow_settings_set_bool(const char *k, int v)
{
if (!strcmp(k, "audio.output_bt")) { g_output_bt = v; return 0; }
if (!strcmp(k, "network.bluetooth_enabled")) { g_bt_en_setting = v; return 0; }
return -1;
}
int meow_settings_get_str(const char *k, char *b, int n)
{
if (!strcmp(k, "bt.codec")) { snprintf(b, (size_t)n, "%s", g_codec); return 0; }
if (b && n > 0) b[0] = 0;
return -1;
}
int meow_settings_set_str(const char *k, const char *v)
{
if (!strcmp(k, "bt.codec")) { snprintf(g_codec, sizeof g_codec, "%s", v); return 0; }
return -1;
}
int meow_settings_save(void) { return 0; }
void paint_sb_right(void) {}
void logmsg(const char *fmt, ...) { (void)fmt; }
void bt_role_refresh(int force) { (void)force; }
meow_sys_state g_sys;
static int g_modal_n = 0; /* M47:可见错误提示(弹窗)计数 */
static char g_modal_line[128] = "";
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur)
{
(void)title; (void)cur;
g_modal_n++;
g_modal_line[0] = 0;
if (item && n > 0 && item[0]) snprintf(g_modal_line, sizeof g_modal_line, "%s", item[0]);
return -1;
}
int meow_subpage_run(sub_page_t *p) { (void)p; return 0; }
/* M47:真实时钟(收敛等待用),毫秒单调。 */
unsigned long long now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long long)ts.tv_sec * 1000ULL + (unsigned long long)(ts.tv_nsec / 1000000);
}
static int g_fail = 0;
#define CHECK(cond, ...) do { if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
/* 在 rows 里找下标:kind==HDR 且 label==lbl;返回该 HDR 的下一行下标起查找 */
static int find_hdr(const sub_row_t *r, int n, const char *lbl)
{
for (int i = 0; i < n; i++) if (r[i].kind == SUB_HDR && r[i].label && !strcmp(r[i].label, lbl)) return i;
return -1;
}
/* 在 [from,n) 找首个 SUB_ACT 且 label 前缀 == pref,返回下标 */
static int find_act(const sub_row_t *r, int n, int from, const char *pref)
{
for (int i = from; i < n; i++)
if (r[i].kind == SUB_ACT && r[i].label && !strncmp(r[i].label, pref, strlen(pref))) return i;
return -1;
}
/* 按 label 精确找任意行(M47 未知态占位行用) */
static int row_label(const sub_row_t *r, int n, const char *lbl)
{
for (int i = 0; i < n; i++)
if (r[i].label && !strcmp(r[i].label, lbl)) return i;
return -1;
}
static void fill_state(void)
{
/* 已连接:Pudding(a2dp 播放中)+ Speaker */
g_conn_n = 2;
snprintf(g_conn[0].mac, sizeof g_conn[0].mac, "33:DE:2E:21:AD:30");
snprintf(g_conn[0].name, sizeof g_conn[0].name, "Pudding");
snprintf(g_conn[1].mac, sizeof g_conn[1].mac, "11:22:33:44:55:66");
snprintf(g_conn[1].name, sizeof g_conn[1].name, "Speaker");
/* 已配对:+ 17OP15(未连) */
g_paired_n = 3;
memcpy(&g_paired[0], &g_conn[0], sizeof g_conn[0]);
memcpy(&g_paired[1], &g_conn[1], sizeof g_conn[1]);
snprintf(g_paired[2].mac, sizeof g_paired[2].mac, "E0:42:6D:79:C5:98");
snprintf(g_paired[2].name, sizeof g_paired[2].name, "17OP15");
/* 附近:Earbuds(未配对 → 附近列出) */
g_near_n = 2;
snprintf(g_near[0].mac, sizeof g_near[0].mac, "AA:BB:CC:DD:EE:FF");
snprintf(g_near[0].name, sizeof g_near[0].name, "Earbuds");
snprintf(g_near[1].mac, sizeof g_near[1].mac, "33:DE:2E:21:AD:30"); /* 已配对 → 附近应过滤 */
snprintf(g_near[1].name, sizeof g_near[1].name, "Pudding");
/* A2DP:Pudding SBC/44100 acquired */
g_eps_n = 1;
snprintf(g_eps[0].mac, sizeof g_eps[0].mac, "33:DE:2E:21:AD:30");
snprintf(g_eps[0].codec, sizeof g_eps[0].codec, "SBC");
snprintf(g_eps[0].rate, sizeof g_eps[0].rate, "44100");
snprintf(g_eps[0].chn, sizeof g_eps[0].chn, "2");
snprintf(g_eps[0].bits, sizeof g_eps[0].bits, "16");
g_eps[0].acquired = 1;
snprintf(g_eps[0].role, sizeof g_eps[0].role, "src");
g_output_bt = 0; /* DAC */
snprintf(g_codec, sizeof g_codec, "auto");
}
int main(void)
{
setbuf(stdout, NULL);
fill_state();
bt_ctx_t c;
memset(&c, 0, sizeof c);
c.full = 1;
c.output_bt = 0;
sub_row_t rows[SUB_ROWS_MAX];
int n = bt_rows(&c, rows, SUB_ROWS_MAX);
printf("== P1 bt_rows 分组骨架 ==\n");
CHECK(find_hdr(rows, n, "输出目标") >= 0, "分组③输出目标存在");
CHECK(find_hdr(rows, n, "已连接设备") > 0, "分组④已连接设备存在");
CHECK(find_hdr(rows, n, "已配对设备") > 0, "分组⑤已配对设备存在");
CHECK(find_hdr(rows, n, "附近设备") > 0, "分组⑥附近设备存在");
CHECK(find_hdr(rows, n, "音质 / 连接管理") > 0, "分组⑦音质/连接管理存在");
CHECK(find_hdr(rows, n, "功耗") > 0, "分组⑧功耗存在");
/* A2DP 摘要(首行 SUB_ROW,主=Pudding,副含 codec/采样率/输出=DAC) */
CHECK(rows[0].kind == SUB_ROW, "行0=A2DP摘要(SUB_ROW)");
CHECK(rows[0].label && !strcmp(rows[0].label, "Pudding"), "摘要主=Pudding (实际=%s)",
rows[0].label ? rows[0].label : "(null)");
CHECK(rows[0].sub && strstr(rows[0].sub, "SBC") && strstr(rows[0].sub, "44100") &&
strstr(rows[0].sub, "输出=DAC"), "摘要副含 codec/采样率/输出=DAC (%s)",
rows[0].sub ? rows[0].sub : "(null)");
printf("== P1 已连接设备(权威状态,含 codec join)==\n");
int hdr = find_hdr(rows, n, "已连接设备");
CHECK(hdr >= 0, "定位已连接 HDR");
int ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci >= 0, "已连接列出 Pudding");
CHECK(ci >= 0 && rows[ci].sub && strstr(rows[ci].sub, "SBC"), "已连接 Pudding sub 含 codec SBC");
CHECK(find_act(rows, n, hdr + 1, "Speaker") >= 0, "已连接列出 Speaker");
printf("== P1 已配对设备(已连者标「已连接」on=1)==\n");
hdr = find_hdr(rows, n, "已配对设备");
CHECK(hdr >= 0, "定位已配对 HDR");
ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci >= 0 && rows[ci].on == 1 && rows[ci].sub && strstr(rows[ci].sub, "已连接"),
"已配对 Pudding 标已连接(on=1)");
ci = find_act(rows, n, hdr + 1, "17OP15");
CHECK(ci >= 0 && rows[ci].on == 0, "已配对 17OP15 未连(on=0)");
printf("== P3 附近设备仅列未配对(已配对者过滤)==\n");
hdr = find_hdr(rows, n, "附近设备");
CHECK(hdr >= 0, "定位附近 HDR");
ci = find_act(rows, n, hdr + 1, "Earbuds");
CHECK(ci >= 0, "附近列出 Earbuds");
ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci < 0, "已配对 Pudding 不在附近列表");
printf("== P2 bt_act 命中 → netmgrd 命令 ==\n");
/* 已连接 Pudding tap → disconnect */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "已连接设备");
ci = find_act(rows, n, hdr + 1, "Pudding");
g_last_cmd[0] = 0;
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_last_cmd, "disconnect:33:DE:2E:21:AD:30") == 0, "已连接 tap → disconnect (实际=%s)", g_last_cmd);
/* 已配对 17OP15(未连)tap → connect */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "已配对设备");
ci = find_act(rows, n, hdr + 1, "17OP15");
g_last_cmd[0] = 0;
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_last_cmd, "connect:E0:42:6D:79:C5:98") == 0, "已配对未连 tap → connect (实际=%s)", g_last_cmd);
/* 输出目标 toggle → audio.output_bt 翻转为 1;bt_rows 摘要改输出=蓝牙 */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "输出目标");
ci = find_act(rows, n, hdr + 1, "输出目标");
bt_act(&c, ci, rows[ci].act);
CHECK(g_output_bt == 1, "输出目标 tap → output_bt=1 (蓝牙)");
n = bt_rows(&c, rows, SUB_ROWS_MAX);
CHECK(rows[0].sub && strstr(rows[0].sub, "输出=蓝牙"), "切蓝牙后 A2DP 摘要=输出=蓝牙");
/* 编解码器循环:auto → SBC */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "音质 / 连接管理");
ci = find_act(rows, n, hdr + 1, "编解码器");
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_codec, "SBC") == 0, "编解码器 auto → SBC (实际=%s)", g_codec);
printf("== P4 M47 开关三态(未知 ≠ 已关)==\n");
int sw = 0;
g_bt_state = -1; /* 真实态未知 */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
CHECK(bt_top_sw(&c, &sw) == 1 && sw == -1, "真实态未知 → top_sw *on=-1 (实际=%d)", sw);
n = bt_rows(&c, rows, SUB_ROWS_MAX);
CHECK(row_label(rows, n, "蓝牙状态未知") >= 0, "未知态 → 行内含「蓝牙状态未知」");
g_bt_state = 0; /* 真实态为关 */
memset(&c, 0, sizeof c); c.full = 1;
sw = -2;
bt_top_sw(&c, &sw);
CHECK(sw == 0, "真实态关 → top_sw *on=0 (实际=%d)", sw);
n = bt_rows(&c, rows, SUB_ROWS_MAX);
CHECK(row_label(rows, n, "蓝牙状态未知") < 0, "真实态已知 → 无未知占位行");
printf("== P5 M47 点击收敛:定稿 / 回滚 + 可见错误 ==\n");
/* 5a 下发成功且真实态已达成 → 立即定稿,无弹窗,settings/状态栏=真实态 */
g_bt_state = 1; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 0;
memset(&c, 0, sizeof c);
unsigned long long t0 = now_ms();
bt_top_sw_set(&c, 1);
unsigned long long dt = now_ms() - t0;
CHECK(g_modal_n == 0, "已达成 → 无错误弹窗 (弹窗数=%d)", g_modal_n);
CHECK(g_bt_en_setting == 1, "已达成 → settings 定稿为 1");
CHECK(g_sys.bt_on == 1, "已达成 → 状态栏 g_sys.bt_on=1");
CHECK(dt < BT_TOGGLE_SETTLE_MS, "已达成 → 不必等满超时 (%llums)", dt);
/* 5b 下发失败、真实态仍为开(要求关)→ 回滚到开 + 可见弹窗 */
g_bt_state = 1; g_onoff_rc = MEOW_ERR_NODEV; g_modal_n = 0; g_bt_en_setting = 1;
memset(&c, 0, sizeof c);
bt_top_sw_set(&c, 0);
CHECK(g_modal_n == 1, "下发失败 → 有可见错误弹窗");
CHECK(g_bt_en_setting == 1, "下发失败 → settings 回滚为真实态 1 (实际=%d)", g_bt_en_setting);
CHECK(g_sys.bt_on == 1, "下发失败 → 状态栏仍为真实态 1");
/* 5c 下发成功但真实态不收敛(要求开、真实一直关)→ 等满上限后回滚 + 弹窗 */
g_bt_state = 0; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 1;
memset(&c, 0, sizeof c);
t0 = now_ms();
bt_top_sw_set(&c, 1);
dt = now_ms() - t0;
CHECK(g_modal_n == 1, "超时未收敛 → 有可见错误弹窗(%s)", g_modal_line);
CHECK(g_bt_en_setting == 0, "超时未收敛 → settings 回滚为真实态 0");
CHECK(g_sys.bt_on == 0, "超时未收敛 → 状态栏=真实态 0");
CHECK(dt >= BT_TOGGLE_SETTLE_MS, "确实等满了上限 (%llums)", dt);
/* 5d 真实态未知 → 等满上限后仍是未知 + 弹窗(绝不把未知显示成成功);
* settings 保留原意图不动(评审 r1 P2-1:写 0 会把「不知道」污染成「确定已关」,
* notif_center/settings_page/app_launcher 都把这个键当显示态/自动拉起依据读)。 */
g_bt_state = -1; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 1;
memset(&c, 0, sizeof c);
bt_top_sw_set(&c, 1);
CHECK(g_modal_n == 1 && strstr(g_modal_line, "无法确认") != NULL,
"未知 → 弹窗明说无法确认 (%s)", g_modal_line);
CHECK(c.bt_on == -1, "未知 → 定稿态仍为 -1 (实际=%d)", c.bt_on);
CHECK(g_bt_en_setting == 1, "未知 → settings 保留原意图 1,不被改写 (实际=%d)", g_bt_en_setting);
printf("== P6 M47 收敛矩阵(真实态 × 下发结果 × 探测 × 超时)==\n");
{
static const struct {
int last, want, ok, probe, to; /* 输入 */
int disp, pend, rb, err; /* 期望输出 */
const char *what;
} m[] = {
{ 1, 1, 1, 1, 0, 1, 0, 0, 0, "真开·下发成功·探测开" },
{ 1, 0, 1, 0, 0, 0, 0, 0, 0, "真开·下发成功·探测关" },
{ 1, 0, 1, 1, 1, 1, 0, 1, 1, "真开·下发成功·未收敛(超时)" },
{ 1, 0, 0, 1, 0, 1, 0, 1, 1, "真开·下发失败·探测仍开" },
{ 1, 0, 0, -1, 0, -1, 0, 1, 1, "真开·下发失败·探测未知 → 未知(不顶替)" },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, "真开·下发失败但探测已关(不误报)" },
{ 0, 1, 1, 1, 0, 1, 0, 0, 0, "真关·下发成功·探测开" },
{ 0, 1, 1, 0, 1, 0, 0, 1, 1, "真关·下发成功·未收敛(超时)" },
{ 0, 1, 0, 0, 0, 0, 0, 1, 1, "真关·下发失败·仍关" },
{ 1, 0, 1, -1, 0, -1, 1, 1, 0, "真开·下发成功·探测未知(继续等)" },
{ 1, 0, 1, -1, 1, -1, 0, 1, 1, "真开·下发成功·探测未知(超时)→未知,不用 last_real 顶替" },
{-1, 1, 1, -1, 1, -1, 0, 1, 1, "未知·下发成功·探测仍未知(超时)" },
{-1, 1, 1, -1, 0, -1, 1, 1, 0, "未知·下发成功·探测仍未知(继续等)" },
};
for (size_t i = 0; i < sizeof m / sizeof m[0]; i++) {
int d = meow_toggle_display(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
int p = meow_toggle_pending(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
int r = meow_toggle_rollback(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
int e = meow_toggle_error(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
CHECK(d == m[i].disp && p == m[i].pend && r == m[i].rb && e == m[i].err,
"%s → disp=%d pend=%d rb=%d err=%d (期望 %d/%d/%d/%d)",
m[i].what, d, p, r, e, m[i].disp, m[i].pend, m[i].rb, m[i].err);
}
}
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}