Skip to content

ADB自动化测试

解释 ADB 自动化测试中的设备选择、等待、退出码、超时、重试与失败工件收集。

基于android-17.0.0_r1
AndroidADB自动化测试Shell Protocol故障诊断

ADB自动化测试 ​

一个自动化脚本通常从“运行 adb shell”开始,却经常在三处失去可解释性:连接到了错误的设备, 把“命令失败”与“transport 断开”混成同一个错误,或者超时后留下了仍在运行的远端进程。可靠的 自动化不是把更多命令串起来,而是把一次测试拆成可观测的状态转换:选择设备、等待状态、执行请求、 读取 stdout/stderr/exit、保存失败上下文、逆序清理。

本文面向已经读过 ADB架构、ADB shell机制 和 dumpsys诊断 的读者。你需要理解 transport、shell: service、 shell protocol 和 Binder dump 的基本边界;不要求预先使用 pytest、JUnit 或设备农场。本文不展开 UIAutomator/Espresso、JUnit runner 和设备租约系统,只解释 ADB 层如何让脚本获得可靠输入,并给出 一个可改造的 Python runner。读完后,你应能判断失败发生在选择、等待、命令、断言还是清理阶段。

1. 连接断言 ​

1.1 adb_commandline ​

Android 17 的 adb_commandline() 先解析 -d、-e、-s、-t 等参数;若没有显式选择,才读取 ANDROID_SERIAL,最后把 selector 交给 adb_set_transport()。-s SERIAL 选择序列号,-t ID 选择当前 ADB server 中的 transport id,-d 选择 USB,-e 选择本地 transport。

源码文件:packages/modules/adb/client/commandline.cpp

cpp
if (transport_type == kTransportAny && serial == nullptr) {
    serial = getenv("ANDROID_SERIAL");
}
adb_set_transport(transport_type, serial, transport_id);
选择方式owner适用场景失败边界
adb -s SERIALserver 的 serial 映射固定物理设备或模拟器serial 不存在或状态不可用
adb -t ID当前 server 的 transport 记录固定一次已建立连接transport 重连后 id 失效
ANDROID_SERIALclient 环境单设备脚本的默认目标被命令行 -s 覆盖
-d / -etransport 类型过滤只跑 USB 或 emulator多个候选仍可能歧义

自动化应在第一个业务步骤前记录 adb get-serialno、adb get-state 和 adb devices -l。这些值是 后续判断“设备换了”还是“测试失败”的输入,不是装饰性日志。

1.2 serial状态 ​

serial 描述设备身份,transport id 描述 ADB server 当前持有的一次 transport。设备重启或断线重连后, serial 可能不变而 transport id 改变。因此长时间运行的 runner 可以用 serial 重新选择设备,但不能 把旧 transport id 当作永久身份。

server socket 也属于隔离边界。并行运行多个 ADB server 时,应显式设置 -P PORT 或 ADB_SERVER_SOCKET,否则一个测试可能看到另一个测试的设备列表、forward 和连接状态。

2. 等待设备 ​

2.1 设备路由 ​

当首个参数匹配 wait-for- 时,adb_commandline() 调用 wait_for_device()。它把 wait-for-device、wait-for-disconnect 等补全为 host service,再通过 ADB server 等待 transport 状态;它不是在 shell 中循环解析 adb devices 输出。

源码文件:packages/modules/adb/client/commandline.cpp

cpp
if (!strncmp(argv[0], "wait-for-", strlen("wait-for-"))) {
    const char* service = argv[0];
    if (!wait_for_device(service)) return 1;
    if (argc == 1) return 0;
    --argc;
    ++argv;
}

adb wait-for-device shell ... 会先等待,再让同一个 client 继续执行后续命令;adb devices 只是 当前快照,没有“之后何时进入可执行状态”的同步语义。send_shell_command() 在设备不可达时也会 等待并重连,但调用方仍需持有总 deadline。

2.2 设备状态 ​

wait-for-device 证明 ADB transport 已进入相应状态,不证明 Android 已完成 boot、PackageManager 已扫描结束或 System UI 已可交互。需要系统服务的测试应在其后增加业务就绪条件,例如:

bash
adb -s SERIAL wait-for-device
adb -s SERIAL shell 'while [ "$(getprop sys.boot_completed)" != 1 ]; do sleep 1; done'
adb -s SERIAL shell cmd package list packages >/dev/null

循环也必须受 host deadline 控制;设备属性永远不变化时,远端循环不会自行退出。

3. 结果约束 ​

3.1 进程输出 ​

设备支持 shell_v2 时,read_and_dump_protocol() 按 packet id 处理 stdout、stderr 和 exit。 若连接意外关闭且没有 exit packet,client 默认返回 255;本地输出消费者提前关闭时则可能得到 SIGPIPE + 128。这些结果不能简化为“命令输出为空”。

结果来源runner 解释
exit=0/非零远端 kIdExit命令自身成功或失败
stderr 有内容kIdStderr诊断文本,不自动等于失败
exit=255 且缺少 exit packet异常断开transport 或进程生命周期问题
local SIGPIPEhost 输出 callback消费者提前关闭,不应重试业务动作

源码文件:packages/modules/adb/client/commandline.cpp

cpp
} else if (protocol->id() == ShellProtocol::kIdExit) {
    exit_code = static_cast<uint8_t>(protocol->data()[0]);
}

ADB 的设备测试直接验证这些边界:test_shell_nocheck_failure 用 false 断言 rc 非零; test_pty_logic 用 stdout、stderr 和 exit 17 断言三路结果;关闭 shell protocol 的 -x 路径则 观察到 stderr 合并到 stdout,并且不能取得远端 exit packet。

3.2 退出码采集 ​

adb shell 'cmd; echo $?' 会让业务输出和状态输出共享 stdout。目标命令自己输出数字、没有换行或 修改 shell 控制流时,解析器可能误判。shell protocol 已提供独立 exit packet,runner 应直接消费 本地 adb 进程返回码,并原样保存 stdout/stderr。

4. deadline/重试 ​

4.1 Deadline ​

ADB 的等待和远端命令都可能长时间阻塞。下面的 host helper 创建独立进程组,在超时后先 TERM, 短暂等待后再 KILL,并保留已经收到的输出:

源码文件:packages/modules/adb/test_device.py

python
import os
import signal
import subprocess

def run_with_deadline(argv, timeout, env=None):
    proc = subprocess.Popen(
        argv,
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        start_new_session=True,
        text=True,
    )
    try:
        out, err = proc.communicate(timeout=timeout)
    except subprocess.TimeoutExpired:
        os.killpg(proc.pid, signal.SIGTERM)
        try:
            out, err = proc.communicate(timeout=2)
        except subprocess.TimeoutExpired:
            os.killpg(proc.pid, signal.SIGKILL)
            out, err = proc.communicate()
        raise TimeoutError((argv, out, err))
    return proc.returncode, out, err

进程组清理只能保证 host 侧 adb 子进程被终止。远端进程是否收到 SIGINT、SIGHUP 或 EOF,取决于 shell 模式和连接关闭路径;测试若创建了明确 pid,应在取消后用只读查询确认它是否仍存在。

4.2 重试边界 ​

连接建立、get-state、dumpsys 等只读动作可以在有界次数内重试。pm install、am start、 wm size 和文件写入可能已经生效,即使 host 超时也不能盲目重放。正确顺序是先查询 owner 状态, 再决定继续等待、恢复或重新执行。

失败阶段自动重试原因
selector 找不到设备否配置错误不会因快速重试消失
transport 短暂 offline有界重试连接尚未建立业务请求
只读 dump 超时可缩小范围后重试不改变系统状态
写命令返回前断开先查询状态请求可能已经提交
断言失败否应收集证据,不应改变现场

5. 测试状态机 ​

5.1 am start ​

一个步骤至少有命令、selector、deadline、断言和清理函数。命令返回后,断言读取的是实际状态 owner; 例如 am start 返回不代表首帧已显示,应再查询 Activity/Window 状态。

阶段runner 保存状态 owner结束条件
prepareserial、server socketADB server selectortransport 唯一且可用
executestdout/stderr/rcadbd 与远端进程exit packet 或连接关闭
assert期望值、观测值Android service/文件系统查询返回并满足条件
collectlogcat/dumpsys 路径诊断服务与 host 文件工件命令有界结束
cleanup逆序动作及结果临时文件、进程、forward每项成功或记录失败

5.2 清理约束 ​

若测试依次创建临时目录、安装包、启动 Activity、建立 forward,清理顺序应反过来:移除 forward、 停止 Activity/应用、卸载包、删除临时目录。清理命令要限定到本次测试创建的资源,重复执行也应安全。

6. 最小runner ​

6.1 步骤记录 ​

教学示例:下面是本文构造的最小 runner,不是 AOSP 源码。

python
from dataclasses import dataclass
from pathlib import Path
import subprocess
import time

@dataclass
class StepResult:
    name: str
    returncode: int
    stdout: str
    stderr: str
    elapsed: float

class AdbRunner:
    def __init__(self, serial, artifact_dir):
        self.adb = ['adb', '-s', serial]
        self.artifacts = Path(artifact_dir)
        self.artifacts.mkdir(parents=True, exist_ok=True)

    def run(self, name, args, timeout=30, check=True):
        start = time.monotonic()
        proc = subprocess.run(
            self.adb + args,
            text=True,
            capture_output=True,
            timeout=timeout,
        )
        result = StepResult(
            name, proc.returncode, proc.stdout, proc.stderr,
            time.monotonic() - start,
        )
        (self.artifacts / f'{name}.stdout').write_text(result.stdout)
        (self.artifacts / f'{name}.stderr').write_text(result.stderr)
        if check and result.returncode != 0:
            raise RuntimeError(result)
        return result

    def prepare(self):
        self.run('wait', ['wait-for-device'], timeout=30)
        state = self.run('state', ['get-state'], timeout=5).stdout.strip()
        if state != 'device':
            raise RuntimeError(f'unexpected state: {state!r}')

check=True 只把非零 rc 变成异常,不丢掉 stdout/stderr;清理可以使用 check=False,但仍需保存其 结果。生产 runner 应把前面的进程组 deadline helper整合进来,因为 subprocess.run(timeout=...) 只负责等待和终止直接子进程,复杂 wrapper 仍需要明确的进程树策略。

6.2 失败工件 ​

失败时至少保存目标 serial、get-state、devices -l、失败步骤的 stdout/stderr/rc、最近一段 logcat -d 和相关 service 的 dumpsys。设备仍可用时再收集 bugreport;设备 offline 时,不能让 bugreport 的二次失败覆盖原始错误。

教学示例:下面是在最小 runner 上补充的失败收集函数,不是 AOSP 源码。

python
def collect_failure(runner):
    runner.run('failure-state', ['get-state'], timeout=5, check=False)
    runner.run('failure-logcat', ['logcat', '-d', '-t', '200'],
               timeout=10, check=False)
    runner.run('failure-activity',
               ['shell', 'dumpsys', 'activity', 'activities'],
               timeout=10, check=False)

adb devices -l 是 host 级命令,不应该带 -s,实际 runner 可另建 run_host()。这个差异也说明: 每个命令都要先判断它由 host server 消费,还是需要具体 transport。

7. 测试边界 ​

7.1 ADB验证 ​

测试输入关键断言没有证明
test_shell_nocheck_failurefalserc 非零、stdout 为空transport 重连策略
test_pty_logicstdout/stderr + exit 17三路结果分别匹配所有旧设备支持 v2
test_non_interactive_sigintsleep 60 后 SIGINT远端 pid 在 3 秒内消失已提交事务会回滚
test_adb.py wait case延迟注册 emulatorwait 后 state=deviceframework 已 boot completed
test_push_error_reporting写只读目录权限或只读错误可见目标文件总能保留

测试的价值不只是“有覆盖率”,而是为 runner 提供输入、断言和证明边界。把同样结构用于业务测试, 可以避免把一次偶然通过写成全局保证。

7.2 失败定位 ​

  1. 立刻报多设备:检查 selector 与 devices -l,不要增加重试次数。
  2. 等待超时:区分设备不存在、unauthorized、offline、ADB ready 与 framework ready。
  3. rc 非零但连接正常:读取 stderr 和 exit,回到远端命令前置条件。
  4. 超时后再次执行产生重复状态:先查询 owner,确认第一次请求是否已经生效。

没有设备时,可以由 Android 17 源码确认 selector 优先级、wait service 路由、shell exit packet 和 测试断言;不能证明某台产品的 boot 时间、厂商服务就绪条件、USB 稳定性或设备农场隔离策略。

8. 运行结果 ​

拿一个真实步骤回答五个问题:selector 是什么,它的生命周期多长;等待的是 transport 还是业务 服务;stdout、stderr、exit 和断开如何区分;断言读取哪个 owner,何时生效;失败收集和逆序清理 分别做什么。

如果这些问题无法回答,脚本仍然只是命令串。能够回答它们,才能把 ADB 的源码行为、runner 的 deadline 策略和 Android 服务的异步状态放在同一条可验证主线上。