先看结论与判断条件
- 每次执行都保留独立回执,首轮失败与后续通过同时存在;重跑用于测量复现分布,不是覆盖先前结果。
- 先区分测试未启动、设备准备失败和应用阶段失败,只有到达目标业务阶段的回执才适合判断加固兼容。
- 失败指纹由测试步骤、失败阶段、退出原因、规范化异常和稳定栈帧组成,并绑定候选摘要与设备配置,不能只按错误文案分组。
- 环境相关需要可重复证据,例如失败跟随特定设备实例或虚拟设备限制,并且控制候选也出现同类问题;单台云设备异常不足以下结论。
- 真实兼容问题可以是稳定失败,也可以是由时序和资源压力放大的偶发失败;不稳定不等于无害,仍需最小变量对照和机制解释。
- 发布门禁至少保留阻断、人工复核、无效执行和有界通过四种状态,任何缺少候选、设备、用例或退出证据的偶发项都不得静默放行。
先把偶发失败当证据问题,而不是重跑问题
一次失败、一次通过只说明结果不稳定,不说明第一次是环境抖动,也不说明第二次代表真实质量。正确分诊要保存每次尝试的候选摘要、设备实例、用例版本、输入数据、开始时间、执行阶段和最终结果,再分析失败是否在相同条件下重复。删除首个失败、只保留最后一次通过,会破坏复现分布,也会让发布记录无法解释风险。
偶发失败至少包含三类可能:测试基础设施没有把用例送到应用,设备或系统状态改变了执行条件,应用内部的竞态、资源边界或平台交互本身不稳定。第三类仍然是真实产品问题,即使它只在特定时序出现。分诊的任务是逐层排除并形成可复查证据,而不是尽快给失败贴上 flake 标签。
发布前应预先规定重跑策略,包括哪些失败允许重跑、每次是否重建设备、是否保持同一实例、何时切换物理设备、何时使用原始候选对照,以及最大调查时间。重跑次数由项目风险和路径重要性决定,不存在适合所有应用的统一数字。策略应在看到结果前确定,避免为了获得通过而不断追加尝试。
| 状态 | 典型证据 | 允许判断 | 下一步 |
|---|---|---|---|
| 无效执行 | 设备准备或测试调度失败,未到达应用阶段 | 当前回执不能评价兼容 | 修复基础设施并重新取得有效执行 |
| 环境相关候选 | 失败跟随实例、设备池或明确平台限制 | 可能与环境变量相关 | 用控制候选和另一实例验证迁移 |
| 候选相关失败 | 同候选同指纹在有效执行中复现 | 存在当前候选兼容风险 | 缩小代码、配置和平台变量 |
| 偶发未判定 | 相同条件既有通过又有失败 | 证据尚不足 | 保留全部回执并继续分层复现 |
| 稳定通过 | 预定义矩阵内均到达并完成目标断言 | 只支持当前覆盖范围 | 记录未测设备与条件 |
| 证据缺失 | 候选、设备、用例或退出记录无法绑定 | 结果不可审计 | 阻断并补采原始回执 |
固定候选、用例和设备条件,才有可比重跑
每轮重跑先固定 candidate_sha256、applicationId、versionCode、签名摘要、构建变体和加固配置摘要。测试脚本、测试数据、账号状态、权限、区域、网络模型与前置安装状态同样要版本化。任何一个字段改变,都应开启新的比较分支;不能用修订后的脚本或重新构建的包覆盖旧候选,再把结果称为同一问题已通过。
设备条件至少包括型号、系统版本、ABI、物理或虚拟类型、设备实例标识、方向、locale 和存储状态。Firebase Test Lab 的矩阵由设备与配置维度组合,设备目录、容量和稳定性也会变化,因此报告应保存实际执行设备,而不是只写计划中的型号。设备池名称相同,不代表每次获得的是同一实例和同一状态。
重跑应同时设计同实例与换实例两种观察。同实例重跑适合确认应用内时序是否可重复,换实例重跑适合判断问题是否跟随设备状态;若直接每次换设备,失败消失可能只是状态迁移。反过来,只在一台已污染实例上重复,也无法代表设备类型。两类回执需要分别标记,不能汇总成一个模糊的通过次数。
- 每次执行绑定同一候选摘要、签名与配置摘要
- 用例代码、数据、账号、权限和前置状态版本化
- 记录实际型号、系统、ABI、locale 与设备实例 ID
- 物理设备、虚拟设备和云设备池明确区分
- 同实例重跑与换实例重跑分别统计
- 任何输入变化都开启新分支而不覆盖旧回执
先判断测试是否真正到达应用失败阶段
云测试失败可能发生在设备分配、系统启动、应用安装、测试包安装、权限准备、用例启动或业务断言阶段。只有应用已安装、目标测试开始并到达规定业务阶段,后续崩溃、ANR 或断言失败才适合进入兼容分诊。设备分配超时或测试 APK 未安装应记为 infra_invalid,不能计作应用通过,也不能计作加固失败。
阶段标记应由测试桩写入结构化回执,例如 provisioned、app_installed、test_started、business_step_reached 和 assertion_completed。日志中出现应用包名不等于测试已到达目标阶段,仪器进程退出也不等于被测应用崩溃。每次失败要保存 last_reached_stage、runner_status 和 app_process_status,避免把测试框架故障归到候选。
依赖真实 Android 运行时、组件和系统 API 的行为应通过设备端 instrumented test 验证,但测试代码自身也可能含有等待条件、选择器或异步同步错误。出现偶发断言时,先确认断言观察的是稳定业务状态,而不是固定延时后的瞬间界面。修复测试同步后必须保留原失败为历史,并用新用例版本建立新的可比序列。
- 设备分配、安装、用例启动和业务阶段分别记录
- infra_invalid 不会被计作候选通过或失败
- runner 进程状态与应用进程状态分别采集
- 断言使用可观察条件而非未经证明的固定等待
- 测试脚本修订后生成新的用例版本
- 历史失败不因基础设施修复而从记录中删除
用稳定字段生成失败指纹,不按整段日志硬匹配
失败指纹用于判断多次失败是否属于同一症状。推荐字段包括 test_id、业务阶段、失败类别、ApplicationExitInfo reason、规范化异常类、稳定的首个项目栈帧以及 Native tombstone 的符号化顶层帧。候选摘要和设备配置作为分布维度保存,不必都塞进症状指纹,否则同一问题在不同设备上会被拆成完全不同的簇。
日志中的时间戳、线程 ID、对象地址、随机文件名、设备临时目录和会话 ID应在脱敏后排除,否则同一失败每次都会生成新指纹。另一方面,不能只保留“崩溃”或“断言失败”两个宽泛标签;失败阶段、异常类型和稳定帧缺失时,不同根因会被错误合并。规范化规则必须版本化,规则变化后重新计算并保留旧映射。
ApplicationExitInfo 可以提供退出原因、ANR trace,并在适用版本返回 Native tombstone。它应与当前候选、时间窗、用户路径和测试阶段绑定。exit reason 相同不等于根因相同,tombstone 顶层帧也可能只是最终表象;指纹的用途是聚类与复现导航,不是自动宣布某个加固模块有缺陷。
| 字段 | 处理方式 | 聚类价值 | 常见误用 |
|---|---|---|---|
| 业务阶段 | 保留稳定阶段 ID | 区分启动、登录、数据库和后台路径 | 只保留页面文案 |
| 退出原因 | 保留平台枚举或稳定分类 | 区分崩溃、ANR 与系统终止线索 | 直接当技术根因 |
| 异常类型 | 保留类名并去除动态消息 | 合并参数不同的同类失败 | 按整段 message 分裂 |
| 稳定栈帧 | 保留符号化项目帧与方法 | 定位共享执行位置 | 使用地址或行号唯一判定 |
| 动态字段 | 去除时间、线程、地址和临时路径 | 避免同症状被拆散 | 未经脱敏进入公开记录 |
| 候选与设备 | 作为分布维度单独保存 | 判断问题跟随候选还是环境 | 全部写入症状哈希导致无法跨组比较 |
通过复现分布判断问题跟随什么变量
拿到失败簇后,先按候选、设备类型、设备实例、系统版本、ABI 和用例版本统计分布。相同指纹若只在一个设备实例出现,换同型号实例后消失,控制候选也在该实例出现,则环境相关性增强;但仍要找到设备状态或基础设施机制。仅凭一台云设备失败、另一台通过,最多标记实例相关候选,不能直接关闭。
相同指纹若在多个实例、物理设备和固定业务阶段跟随加固候选出现,而原始对照在相同条件下不出现,则候选相关性增强。若失败频率受调度、内存或网络压力影响,它仍可能是应用竞态或超时边界,不应因偶发而归类环境。分诊还需要缩小加固配置或代码路径,并通过恢复性对照解释机制。
原始对照和加固候选都出现同一失败,也不能立刻宣布与加固无关。加固可能改变触发概率,而测试基础线本身也可能有缺陷。允许的结论是当前症状不是加固候选独有,需要继续比较阶段、分布和机制。没有足够重复样本时不要输出百分比或稳定率承诺,保留原始计数和每次回执更诚实。
| 分布观察 | 当前判断 | 必需对照 | 发布动作 |
|---|---|---|---|
| 未到达应用阶段 | 执行无效 | 修复设备或 runner 后重跑 | 不放行也不归因候选 |
| 只在单一实例出现 | 实例相关候选 | 同型号新实例与控制候选 | 人工复核 |
| 只在虚拟设备出现 | 可能受虚拟化限制影响 | 物理设备与功能限制核对 | 关键路径保持阻断 |
| 跨实例跟随同一候选和指纹 | 候选相关性增强 | 原始对照与最小配置对照 | 阻断 |
| 原始与加固候选都出现 | 不是候选独有 | 比较频次、阶段和共享机制 | 按业务风险阻断并定位 |
| 同条件既通过又失败 | 真实偶发项尚未关闭 | 固定条件重复与压力变量隔离 | 人工复核或阻断 |
物理设备、虚拟设备和云容量各有证据边界
Test Lab 可用设备目录、稳定性和容量会变化,测试计划应记录实际型号与系统回执。设备暂时不可用、排队或容量调整属于服务环境事实,不代表候选兼容结论。若用例被重新调度到另一个设备配置,必须新建执行记录,并标记 changed_environment,不能悄悄并入同实例重跑序列。
AVD 在 ABI、图形、Play Store 和旧 API 等方面有明确限制。关键 Native 加固路径、硬件相关能力、特定厂商服务或商店依赖如果超出 AVD 边界,虚拟设备通过不能替代物理设备验收;虚拟设备失败也要先核对是否命中已知限制。限制匹配只是环境解释线索,仍需物理设备对照才能关闭。
物理云设备也不是绝对真值。它可能存在历史状态、温度、电量、网络、存储和系统后台活动差异。测试应在每轮采集与业务相关的环境快照,并执行受控重置。若失败只跟随资源状态,应继续判断应用是否对该状态有合理处理;真实设备压力暴露的竞态仍可能是产品缺陷,不能统称云平台抖动。
- 保存实际执行设备而非只保存计划矩阵
- 重新调度到新实例时标记环境变化
- AVD 限制与目标 ABI、图形和商店依赖逐项核对
- 关键 Native 路径包含物理设备验收
- 物理设备的网络、存储、电量和后台状态按需采集
- 环境压力暴露的应用竞态不会被自动豁免
用只读脚本聚类失败指纹和复现分布
下面的 Python 脚本读取脱敏多轮回执,对有效应用失败生成指纹,并按候选、测试、设备配置统计通过与失败。输入必须提供候选 SHA-256、test_id、device_profile、device_instance、reached_stage 和 result。失败记录还要提供 failure_phase、exit_reason、exception_class 与 stable_frame;脚本不会读取完整日志或输出敏感动态字段。
指纹只使用症状字段,因此同一失败可以跨候选和设备比较;候选与设备保留为分布维度。脚本把同一 scope 内既有通过又有失败的项列为 intermittent,把只失败的项列为 reproducible,并把未到达应用阶段的记录单列 invalid_runs。分类只是排查队列,不宣称环境或加固根因,所有偶发簇都进入 human_review。
代码要求调用方明确提供每次回执,拒绝缺少字段、错误候选摘要、未知结果和失败证据不完整的输入。实际系统还应保存不可变原始回执、规范化规则版本和时间关联,并把 ApplicationExitInfo 或 tombstone 的证据摘要绑定到 run_id。一次聚类通过不代表发布通过,它只减少人工比较重复日志的成本。
import hashlib
import json
import sys
from collections import defaultdict
from pathlib import Path
VALID_RESULTS = {"pass", "fail", "infra_invalid"}
BASE_FIELDS = {
"run_id",
"candidate_sha256",
"test_id",
"device_profile",
"device_instance",
"reached_stage",
"result",
}
FAILURE_FIELDS = {"failure_phase", "exit_reason", "exception_class", "stable_frame"}
def stop(message):
raise SystemExit(f"invalid flake receipts: {message}")
def digest(fields):
encoded = json.dumps(fields, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()[:20]
if len(sys.argv) != 2:
raise SystemExit("usage: python flake_triage.py RECEIPTS_JSON")
receipt_path = Path(sys.argv[1]).resolve()
if not receipt_path.is_file():
stop(f"file not found: {receipt_path}")
payload = json.loads(receipt_path.read_text(encoding="utf-8"))
records = payload.get("records")
if not isinstance(records, list) or not records:
stop("records must be a non-empty list")
scopes = defaultdict(list)
clusters = defaultdict(list)
invalid_runs = []
for index, record in enumerate(records):
if not isinstance(record, dict):
stop(f"record {index} must be an object")
missing = sorted(BASE_FIELDS.difference(record))
if missing:
stop(f"record {index} missing {missing}")
candidate = record["candidate_sha256"]
result = record["result"]
if not isinstance(candidate, str) or len(candidate) != 64:
stop(f"record {index} has invalid candidate_sha256")
if result not in VALID_RESULTS:
stop(f"record {index} has invalid result")
scope = (candidate, record["test_id"], record["device_profile"] )
scopes[scope].append(record)
if result == "infra_invalid":
invalid_runs.append(record["run_id"])
continue
if result == "fail":
absent = sorted(FAILURE_FIELDS.difference(record))
if absent:
stop(f"record {index} missing failure fields {absent}")
symptoms = {field: record[field] for field in sorted(FAILURE_FIELDS)}
symptoms["test_id"] = record["test_id"]
clusters[digest(symptoms)].append(record)
review = []
for scope, attempts in sorted(scopes.items()):
valid = [item for item in attempts if item["result"] != "infra_invalid"]
passed = sum(item["result"] == "pass" for item in valid)
failed = sum(item["result"] == "fail" for item in valid)
if failed:
review.append({
"candidate_sha256": scope[0],
"test_id": scope[1],
"device_profile": scope[2],
"valid_runs": len(valid),
"failed_runs": failed,
"passed_runs": passed,
"disposition": "intermittent" if passed else "reproducible",
"human_review": True,
})
cluster_rows = [{"fingerprint": key, "runs": [item["run_id"] for item in value]} for key, value in sorted(clusters.items())]
print(json.dumps({"failure_clusters": cluster_rows, "review": review, "invalid_runs": invalid_runs}, ensure_ascii=False, indent=2))门禁用有界结论关闭,不用最后一次结果关闭
发布门禁应把所有有效失败保留为开放证据,直到获得可解释的处置。候选相关的同指纹复现、关键物理设备失败、原始与加固差分异常、偶发项缺少机制解释、环境分类没有控制候选证明,以及回执无法绑定当前候选,都应阻断或进入人工风险审批。最后一次重跑通过不能自动把状态改绿。
环境相关的关闭也需要证据链:失败未到达应用阶段,或同一问题稳定跟随特定设备实例、已知虚拟设备限制或基础设施事件;换健康实例后有效执行完成;控制候选表现与解释一致;原始回执仍然保留。即使环境原因成立,也应修复测试稳定性,避免未来把真正的候选失败淹没在噪声里。
通过结论只覆盖预定义候选、设备、ABI、用例版本和环境范围,未测项必须列出。NIST SP 800-218 SSDF 强调保留来源、构建、验证与变更证据,适合约束发布记录,但不定义加固产品功能。需要分诊实际偶发回归时,可通过御盾中央平台提交脱敏候选摘要、设备回执、失败指纹、退出证据和重跑分布,先确认执行有效,再定位兼容变量。
- 首次失败与所有重跑回执永久保留
- 候选相关复现和关键物理设备失败直接阻断
- 环境结论包含迁移对照、控制候选和明确机制
- 偶发项没有因最后一次通过而自动关闭
- 发布结论列出候选、设备、ABI、用例和未测范围
- 调查材料只含脱敏指纹与可复核原始回执引用
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android 云测试矩阵由设备型号、系统、方向和 locale 等维度共同定义。 | Firebase Test Lab Android matrices 描述 Android 设备矩阵配置以及执行失败对矩阵结果的影响。 | 云端矩阵仍需依据真实用户与最低支持范围选择,单一矩阵不能代表全部设备和业务路径。 |
| 云测试设备目录、稳定性与容量会发生变化,报告应保存实际执行设备。 | Test Lab available devices 说明可用设备、容量与稳定性属性会变化,并提供设备目录信息。 | 设备可用不代表覆盖目标业务的全部厂商特性,也不能把一次设备异常直接归因于环境。 |
| 虚拟设备在 ABI、图形、Play Store 和旧 API 等方面存在明确限制。 | Firebase Test Lab AVD limits 列出 AVD 的系统版本、ABI、图形和 Play Store 等限制。 | 虚拟设备通过不能替代关键 Native 路径的物理设备验收,虚拟设备失败也不自动证明候选缺陷。 |
| 依赖真实 Android 运行时、组件与系统 API 的行为应通过设备端测试验证。 | Android instrumented tests 说明 instrumented test 在 Android 环境运行并可访问真实框架和组件。 | 单一设备或一次通过不能代表完整 API、ABI 与厂商矩阵,测试代码自身也需要稳定性验证。 |
| ApplicationExitInfo 可提供进程退出原因、ANR trace,并在适用版本返回 Native tombstone。 | Android ApplicationExitInfo 描述历史进程退出信息、相关 trace 和可用的 Native tombstone。 | 退出记录必须绑定同一候选、时间窗和用户路径,退出原因或栈帧不能单独证明根因。 |
| 安全发布应保留来源、构建、验证和变更证据。 | NIST SP 800-218 SSDF 把来源追溯、构建完整性、验证与持续维护纳入安全软件开发实践。 | SSDF 是组织级框架,不定义某个 App 加固产品的功能,也不提供偶发失败判定阈值。 |
| 重跑通过不能删除首次失败,应保留全部尝试并分析复现分布。 | 工程判断:只保存最后结果会破坏候选、设备与失败指纹之间的可追溯关系,无法区分偶发风险和无效执行。 | 重跑次数、停止规则和发布风险阈值需要项目预先定义,文章不提供通用数字。 |
| 偶发失败可能是真实应用竞态,不能因不稳定而自动归为环境抖动。 | 工程判断:只有失败随明确环境变量迁移、控制候选表现一致且应用阶段证据可解释时,环境相关性才足以关闭候选归因。 | 没有当前候选、设备实例、用例版本、退出记录和对照分布时,不能宣布加固兼容通过或环境根因。 |
工程常见问题
加固回归第一次失败、第二次通过,可以按通过发布吗?
不可以直接这样处理。两次结果共同证明不稳定,应保留首个失败,核对执行阶段和失败指纹,再按预设重跑策略分析分布。
单台云设备失败是否通常就是环境问题?
不是。它只能形成实例相关线索,需要同型号新实例、控制候选、物理设备和明确环境机制对照,才能加强环境解释。
怎样判断一次云测试根本没有测到应用?
查看设备准备、应用安装、测试启动和业务阶段标记;若未到达规定应用阶段,应记为无效执行,而非候选失败或通过。
失败指纹为什么不能直接使用整段异常日志?
整段日志常含时间、线程、地址和临时路径,会把同一问题拆散。应保留阶段、退出原因、异常类型和稳定栈帧。
AVD 上不复现能否证明 Native 加固路径没问题?
不能。AVD 在 ABI、图形、商店和旧 API 上有边界,关键 Native 路径仍需目标物理设备和实际 ABI 回执。
提交偶发兼容问题需要哪些最小材料?
准备候选摘要、用例版本、设备实例、执行阶段、失败指纹、ApplicationExitInfo 或 trace 引用、全部重跑结果和控制候选对照。