跨端: fix(设备闸): 按真机形状改写解析(前台 = state #FOREGROUND 那个 mission 的 bundle),判据改喂真机样本
第一版是**猜的**格式(`bundleName: com.x`),真机上根本没有这种写法;退一步"取第一个 bundle name" 又会取到别人的(真机多 mission,`#31 com.example.homeagent` 排在我们 `#32` 前)⇒ 假绿(pi 独立复现)。 真机形状(样本 `test/fixtures/aa-dump-l-real.txt`,实采):每块 `Mission ID #N … ` + `bundle name [com.x]` + `state #FOREGROUND/#BACKGROUND`;**前台只由 `state #FOREGROUND` 决定**。 - `parseMissions` / `foregroundBundle`:按块解析,取不到返回空(不猜、不兜底); - `foregroundVerdict` 三态不变:`ours` / `other` / `unverified`; - 有 mission 但**全无 FOREGROUND** ⇒ `unverified`(不是"没我们所以算别人"); - 空 dump / 截断 ⇒ `unverified`,状态词写明"**缺证据 ≠ 没有那个现象**"。 判据 5 条(喂真样本 + 对调状态 + 空/截断/无前台 + 撞名撞文案反证)全绿。 真样本本身含"对方在前台"那一刻 —— 争用在一分钟内真实发生过,所以这不是构造出来的场景。
This commit is contained in:
@ -12,53 +12,75 @@
|
||||
* 一句话:**"没验"和"验过了"之间不许有斜坡。**
|
||||
*/
|
||||
|
||||
/** 我们从 dump 里真正要用到的两样东西 */
|
||||
export interface ForegroundInfo {
|
||||
bundle: string;
|
||||
windowTitle: string;
|
||||
}
|
||||
|
||||
/** 闸的判决:**只有三种**,没有第四种"大概是我们" */
|
||||
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;
|
||||
}
|
||||
|
||||
/** `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+)');
|
||||
|
||||
/**
|
||||
* 从 dump 文本里取"谁在前台"。
|
||||
* 解析所有 mission 块。取不到就返回**空数组**(空 dump / 截断 / 格式变了)—— 不猜、不兜底。
|
||||
*
|
||||
* **取不到就返回 undefined**(空 dump / 截断 / 格式变了)—— 不猜、不兜底。
|
||||
* 空 dump 是最容易被误读的一种:**窗口还没起来**时它最常出现,
|
||||
* 而"没读到"极容易被当成"那个坏现象没有出现"(⇒ 假绿)。
|
||||
* ⚠️ 第一版是**猜的**格式(`bundleName: com.x`),真机上根本没有那种写法;改"取第一个 bundle name"
|
||||
* 又会在多 mission 时取到**别人的**(真机上对方可能排在我们前面)⇒ 假绿。真机形状见文件头。
|
||||
*/
|
||||
export function parseForeground(dump: string): ForegroundInfo | undefined {
|
||||
export function parseMissions(dump: string): MissionInfo[] {
|
||||
const out: MissionInfo[] = [];
|
||||
if (dump.length === 0) {
|
||||
return undefined;
|
||||
return out;
|
||||
}
|
||||
const bundle: string = firstMatch(dump, 'bundleName');
|
||||
const altBundle: string = bundle.length > 0 ? bundle : firstMatch(dump, 'bundle');
|
||||
if (altBundle.length === 0) {
|
||||
return undefined;
|
||||
const lines: string[] = dump.split('\n');
|
||||
let bundle: string = '';
|
||||
let state: 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 });
|
||||
}
|
||||
bundle = '';
|
||||
state = '';
|
||||
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];
|
||||
}
|
||||
}
|
||||
const info: ForegroundInfo = { bundle: altBundle, windowTitle: firstMatch(dump, 'windowTitle') };
|
||||
return info;
|
||||
if (bundle.length > 0) {
|
||||
out.push({ bundle: bundle, state: state });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某个键的值(**限定在同一行内**)。
|
||||
*
|
||||
* ⚠️ 这里踩过一次坑,写下来免得再犯:第一版写的是 `key + '[^A-Za-z0-9_]*([A-Za-z0-9_.]+)'`,
|
||||
* 那个否定类**既接受非 ASCII 也接受换行** ⇒ 对 `windowTitle: 邮件\n abilityName: EntryAbility`
|
||||
* 它会一路吞掉中文标题和换行,然后**捕获下一行的键名** `abilityName`。
|
||||
* 于是"标题"读到的是别人的键名 —— 一个**看起来有值、其实指错地方**的结果(判据当场把它抓出来了)。
|
||||
* ⇒ 分隔符不许跨行:按行取,值里允许中文。**空值返回空串,绝不回退到别处找**。
|
||||
* 前台是哪个 bundle —— **只认带 `state #FOREGROUND` 的那个 mission 块**。
|
||||
* 取不到返回空串(没有 mission / 没有 FOREGROUND / 空 dump):
|
||||
* **空 dump 最容易被误读**(窗口没起来时它最常出现,而"没读到"极易被当成"坏现象没有出现")。
|
||||
*/
|
||||
function firstMatch(text: string, key: string): string {
|
||||
const re = new RegExp(key + '\\s*[:=]\\s*([^\\n\\r]*)');
|
||||
const m = re.exec(text);
|
||||
if (m === null) {
|
||||
return '';
|
||||
export function foregroundBundle(dump: string): string {
|
||||
const missions: MissionInfo[] = parseMissions(dump);
|
||||
for (const m of missions) {
|
||||
if (m.state === 'FOREGROUND') {
|
||||
return m.bundle;
|
||||
}
|
||||
}
|
||||
return m[1].trim();
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,11 +90,11 @@ function firstMatch(text: string, key: string): string {
|
||||
* 文案相同的项("邮件"这种通用词尤其容易)。按文案判 ⇒ 别人的 dump 会被当成我们的 ⇒ 静默放行。
|
||||
*/
|
||||
export function foregroundVerdict(dump: string, ourBundle: string): string {
|
||||
const info: ForegroundInfo | undefined = parseForeground(dump);
|
||||
if (info === undefined) {
|
||||
const fg: string = foregroundBundle(dump);
|
||||
if (fg.length === 0) {
|
||||
return VERDICT_UNVERIFIED;
|
||||
}
|
||||
return info.bundle === ourBundle ? VERDICT_OURS : VERDICT_OTHER;
|
||||
return fg === ourBundle ? VERDICT_OURS : VERDICT_OTHER;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,5 +113,5 @@ export function unverifiedReason(verdict: string): string {
|
||||
if (verdict === VERDICT_OTHER) {
|
||||
return '未验:前台是别的 app(拿不到独占窗口)—— 读到的界面不是我们的,断言会讲错对象';
|
||||
}
|
||||
return '未验:没读到前台是谁(空 dump / 截断 / 格式变了)—— **缺证据不等于没有那个现象**';
|
||||
return '未验:没读到前台是谁(空 dump / 截断 / 格式变了 / 没有 FOREGROUND 状态)—— **缺证据不等于没有那个现象**';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user