/** * 设备读回的**前置闸**(纯逻辑,无 `@ohos`)—— 判据能用 node 直接跑。 * * 为什么需要它(pi 邮件 `971c58fa` §4):别的 agent 在同一个模拟器上跑它的 app, * 它的 `aa start` 会把前台抢走。如果我在那之后读控件树,**读到的是它的窗口**, * 而我的断言就会拿**别的 app 的布局**去判 —— 可能"通过",也可能红,**两者都不是在讲我们的界面**。 * 后者是**假绿**:比"被打断"严重得多。 * * 处置规则(pi 邮件 `82ae145b` 认可,`bf583b0f` 补样本): * 前台不是我们 ⇒ **不下断言**;而且**不许静默跳过**(跳过会在下一轮被读成"验过了")。 * 正确状态是 **`未验`**,余额里那笔到期欠账**继续开着**。 * 一句话:**"没验"和"验过了"之间不许有斜坡。** */ /** 闸的判决:**只有三种**,没有第四种"大概是我们" */ export const VERDICT_OURS: string = 'ours'; export const VERDICT_OTHER: string = 'other'; export const VERDICT_UNVERIFIED: string = 'unverified'; /** 一个 mission 块里我们要的两样 */ export interface MissionInfo { bundle: string; state: string; /** 同一块里的 `app state #X` —— 真机 dump 两个都在。**两者矛盾 ⇒ 拿不准**(pi 邮件 `6f902e1e`) */ appState: string; } /** `bundle name [com.x]` —— 方括号形状是**真机实测**的(样本 fixtures/aa-dump-l-real.txt) */ const RE_BUNDLE = new RegExp('^\\s*(?:bundle|app)\\s+name\\s+\\[([^\\]]+)\\]'); /** 只认行首的 `state`;`app state #FOREGROUND` 是同一块里的另一行,不算错 */ const RE_STATE = new RegExp('^\\s*state\\s+#(\\w+)'); /** `app state #FOREGROUND` —— 与 `state` 交叉验证:**矛盾就是拿不准**,不是"挑一个信" */ const RE_APPSTATE = new RegExp('^\\s*app\\s+state\\s+#(\\w+)'); /** * 解析所有 mission 块。取不到就返回**空数组**(空 dump / 截断 / 格式变了)—— 不猜、不兜底。 * * ⚠️ 第一版是**猜的**格式(`bundleName: com.x`),真机上根本没有那种写法;改"取第一个 bundle name" * 又会在多 mission 时取到**别人的**(真机上对方可能排在我们前面)⇒ 假绿。真机形状见文件头。 */ export function parseMissions(dump: string): MissionInfo[] { const out: MissionInfo[] = []; if (dump.length === 0) { return out; } const lines: string[] = dump.split('\n'); let bundle: string = ''; let state: string = ''; let appState: string = ''; for (const raw of lines) { const line: string = raw.replace('\r', ''); if (line.indexOf('Mission ID') >= 0) { if (bundle.length > 0) { out.push({ bundle: bundle, state: state, appState: appState }); } bundle = ''; state = ''; appState = ''; continue; } const mb = RE_BUNDLE.exec(line); if (mb !== null && mb[1].length > 0 && bundle.length === 0) { bundle = mb[1]; continue; } const ms = RE_STATE.exec(line); if (ms !== null && state.length === 0) { state = ms[1]; continue; } const ma = RE_APPSTATE.exec(line); if (ma !== null && appState.length === 0) { appState = ma[1]; } } if (bundle.length > 0) { out.push({ bundle: bundle, state: state, appState: appState }); } return out; } /** * 前台是哪个 bundle —— **只认带 `state #FOREGROUND` 的那个 mission 块**。 * 取不到返回空串(没有 mission / 没有 FOREGROUND / 空 dump): * **空 dump 最容易被误读**(窗口没起来时它最常出现,而"没读到"极易被当成"坏现象没有出现")。 */ export function foregroundBundle(dump: string): string { const missions: MissionInfo[] = parseMissions(dump); for (const m of missions) { if (m.state !== 'FOREGROUND') { continue; } // 真机 dump 里 `state` 与 `app state` 是一致的;**两者矛盾 ⇒ 这份 dump 自相矛盾 ⇒ 拿不准** // (不许"挑一个信":那是把互相打脸的证据当成证据。pi 邮件 `6f902e1e` 抓到我的对调样本正是这种形状) if (m.appState.length > 0 && m.appState !== 'FOREGROUND') { return ''; } return m.bundle; } return ''; } /** * 判决"这份 dump 是不是我们的" —— **只许看 bundle,不许看标题/控件名/文案**。 * * 理由(pi 邮件 `bf583b0f` §2):别人的合法 dump 里**完全可能**有与我们要判的控件同名、 * 文案相同的项("邮件"这种通用词尤其容易)。按文案判 ⇒ 别人的 dump 会被当成我们的 ⇒ 静默放行。 */ export function foregroundVerdict(dump: string, ourBundle: string): string { const fg: string = foregroundBundle(dump); if (fg.length === 0) { return VERDICT_UNVERIFIED; } return fg === ourBundle ? VERDICT_OURS : VERDICT_OTHER; } /** * 能不能在这份 dump 上做断言。 * `false` ⇒ 调用侧**必须**把这条判据记成 `未验`(欠账继续开着),**不许**记成通过、也不许静默跳过。 */ export function mayAssertOn(dump: string, ourBundle: string): boolean { return foregroundVerdict(dump, ourBundle) === VERDICT_OURS; } /** 拿不到独占时的状态词 —— 统一到这里,免得各处自己编(编出来的词会互相读不懂) */ export function unverifiedReason(verdict: string): string { if (verdict === VERDICT_OURS) { return ''; } if (verdict === VERDICT_OTHER) { return '未验:前台是别的 app(拿不到独占窗口)—— 读到的界面不是我们的,断言会讲错对象'; } return '未验:没读到前台是谁(空 dump / 截断 / 格式变了 / 没有 FOREGROUND 状态)—— **缺证据不等于没有那个现象**'; }