关于 scripting-adb-for-ci
面向 Android CI 的 ADB 脚本编写技能,帮助团队可靠地运行仪器测试并管理多台设备。内容涵盖退出码判断、并行分发、重试、超时及清理流程。
主要功能
- 01解析仪器测试状态码
- 02多设备并行与测试分片
- 03传输错误重试和超时控制
- 04端口转发及清理
- 05失败时采集诊断产物
- 06Test Orchestrator 配置
适用场景
- 01在 CI 中准确判定 Android 测试结果
- 02在多台设备上并行运行测试
- 03避免重试或清理遗漏导致作业失败
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill scripting-adb-for-ci安装后在 Claude Code 中输入/scripting-adb-for-ci确认已加载。
详细介绍
CI 中的 ADB 脚本——Bash 惯用写法、退出码与扇出
这项技能是将其他 ADB 技能串联成 CI 流水线的纽带:退出码传递、重试、超时、多设备并行扇出、端口转发、幂等设置、失败时捕获,以及 Test Orchestrator 接入。每个部分都会交叉链接到负责底层基本操作的技能。
何时使用这项技能
- CI 任务调用
adb shell am instrument,即使测试失败也会“通过”——它根据$?判断结果,但am instrument总是以0退出;真正的状态位于 stdout 中的INSTRUMENTATION_STATUS_CODE行(而且只有加上-w才能获取这些 stdout)。 - 团队在单个 Agent 上连接了多台实体设备,并希望并行运行分片测试。
- 团队混淆了
adb forward和adb reverse——大多数团队都至少遇到过一次因参数顺序相反而导致的 Bug。 - 遇到暂时性的
error: device not found/error: closed错误时不会重试;任务会因第一次故障而失败。 - 脚本缺少清理步骤,导致遗留的端口转发或修改过的
settings put global ...影响下一次运行。 - 团队使用
androidTestImplementation接入 Test Orchestrator——构建不会报错,却会安装 orchestrator APK 失败。
何时不使用这项技能
- 尚未了解单项基本操作(logcat、screencap、pull、push、settings、input、am instrument)。请先阅读相关的同级技能。
- CI 本身(Gradle 插件、Firebase Test Lab 云配置)。这一层请参考 FTL 自己的文档。
- Compose / instrumentation 测试机制。请使用
../../../compose/synchronization/synchronizing-with-idle/SKILL.md及相关文档。
前置条件
- Bash(大部分操作也适用于 zsh;macOS 用户如果要使用
timeout,应运行brew install coreutils安装gtimeout)。 - 主机上安装 Platform Tools 24+。旧版工具链中,
adb shell的退出码不可靠。 - 设备运行 API 24+,才能可靠地传递 shell 退出码。Nougat 以前的设备可在 stdout 中使用哨兵值
__ADB_RC=$?,再在主机端使用 grep 匹配。
退出码——adb 如何传递退出码
| 操作 | 行为 |
|---|---|
adb push / adb pull | 成功时返回 0,传输或文件系统失败时返回非零值。务必检查。 |
adb install / adb uninstall | 成功时返回 0。现代 adb 在遇到 INSTALL_FAILED_* 时会返回非零值,但旧版本即使包管理器操作失败也会以 0 退出——请通过管道检查 stderr。 |
adb wait-for-device(以及 wait-for-usb-device、wait-for-local-recovery 等) | 阻塞直到达到指定状态,然后退出并返回 0。实用的启动门控:`adb wait-for-device shell getprop sys.boot_completed |
adb shell <cmd> | 在 platform-tools ≥ 24 且设备 adbd ≥ Android 7.0(Nougat / API 24)时,会传递设备端命令的退出码。更旧的设备无论命令结果如何,始终返回 0。 |
adb -t <transport-id> | 传输 ID,不是超时时间。这是脚本中最常见的混淆点。 |
am instrument | 始终以 0 退出,无论测试通过还是失败——AOSP Instrument.java 会在 run() 末尾无条件调用 System.exit(0),而 AndroidJUnitRunner 不论测试结果如何都会调用 finish(Activity.RESULT_OK, …)。-w 会让 shell 等待 Runner 运行完成,以确保其 stdout 完整;不加 -w 时,shell 会立即返回,无法获得可用内容。无论哪种情况,都应根据解析后的 INSTRUMENTATION_STATUS_CODE 行(如下所示)来控制 CI,绝不要根据 $? 判断。 |
可以使用 -x 禁用 adb shell 退出码传递:
shell [-e ESCAPE] [-n] [-Tt] [-x] [COMMAND...]
-x: disable remote exit codes and stdout/stderr separation
在 CI 中几乎绝不会想要使用 -x。默认行为才是你需要的。
AndroidJUnitRunner 状态码
am instrument -w -r 会针对每项测试输出 INSTRUMENTATION_STATUS_CODE: <int>,随后输出最终的 INSTRUMENTATION_CODE: <int>:
| 代码 | 符号名称 | 含义 |
|---|---|---|
1 | REPORT_VALUE_RESULT_START | 测试已开始 |
0 | REPORT_VALUE_RESULT_OK | 测试通过 |
-1 | REPORT_VALUE_RESULT_ERROR | 意外错误(进程死亡、运行器故障) |
-2 | REPORT_VALUE_RESULT_FAILURE | 断言失败 |
-3 | REPORT_VALUE_RESULT_IGNORED(AndroidJUnitRunner 扩展) | 被 @Ignore 忽略/过滤掉 |
-4 | REPORT_VALUE_RESULT_ASSUMPTION_FAILURE(AndroidJUnitRunner 扩展) | JUnit assumeXxx 返回 false |
不要将 -3 或 -4 视为失败——它们表示跳过;只有 -1 和 -2 是失败。末尾的 INSTRUMENTATION_CODE 行与测试结果无关:它是一个 Activity.RESULT_* 值(-1 = RESULT_OK = 运行器级别未崩溃;0 = RESULT_CANCELED = 运行器已退出),与 shell $?(始终为 0)一样,不反映测试是否通过。请在 stdout 中搜索每项测试的 INSTRUMENTATION_STATUS_CODE: -1 / -2(参见下方 WRONG/RIGHT 示例)。
端口转发——adb forward 与 adb reverse
参数顺序相反。 这是最常见的脚本错误。
adb forward [--no-rebind] LOCAL REMOTE # host → device. LOCAL REMOTE
adb reverse [--no-rebind] REMOTE LOCAL # device → host. REMOTE LOCAL
| 方向 | 用例 | 命令示例 |
|---|---|---|
adb forward | 主机需要驱动设备端的服务。 | adb forward tcp:9222 localabstract:chrome_devtools_remote |
adb reverse | 设备需要访问仅在本地主机上运行的服务(CI 运行器、开发笔记本电脑)。 | adb reverse tcp:8080 tcp:8080(Metro/本地后端) |
令牌语法(摘自 adb help):
| 令牌 | 位置 |
|---|---|
tcp:<port> | 任意一端。LOCAL 端的 tcp:0 = 选择任意空闲端口;所选端口会输出到 stdout。 |
localabstract:<name> | Linux 抽象 Unix 域套接字。供 Chrome DevTools / Stetho / Flipper 使用。 |
localreserved:<name> | 保留的 Unix 套接字命名空间。 |
localfilesystem:<name> | 基于文件系统的 Unix 套接字。 |
jdwp:<pid> | 仅限 REMOTE 端。指定设备端 pid 的 Java Debug Wire Protocol。 |
vsock:<CID>:<port> | 仅限 REMOTE 端。virtio-vsock(模拟器)。 |
dev:<char-device> | 字符设备直通。 |
acceptfd:<fd> | 仅限 LOCAL 端——在预先打开的 fd 上监听。 |
清理时使用相同的命令:--list、--remove、--remove-all。务必在 trap 中移除添加的转发,以免在同一 Agent 上重新运行时因“cannot bind: address already in use”而失败。
超时——使用 timeout 包装,而不是 adb -t
adb -t <transport-id> 与超时无关。主机端没有按命令设置 adb 超时的标志。请使用 GNU/BSD timeout 实用工具进行包装:
timeout 60s adb -s emulator-5554 shell am instrument -w -r ...
echo $?
# 124 = timeout reached (process killed); 137 = had to escalate to SIGKILL.
# 0 just means am instrument exited (it always does) — see the timeout pattern below for pass/fail parsing.
在 macOS 上,timeout 是来自 coreutils 的 gtimeout(brew install coreutils)。
对于后台捕获,请设置最长时长,以免卡住的任务占满磁盘:
( timeout 600s adb logcat -v threadtime > artifacts/logcat.txt ) &
LOGCAT_PID=$!
trap 'kill $LOGCAT_PID 2>/dev/null' EXIT
瞬时错误时重试
常见错误包括:error: device 'XYZ' not found、error: closed、error: protocol fault、error: device offline。执行 kill-server 并重新连接通常可以解决大多数问题。
retry() {
local n=0 max=3 delay=2
until "$@"; do
n=$((n+1))
[ "$n" -ge "$max" ] && return 1
echo "[retry] $* failed (attempt $n); restarting adb"
adb kill-server; adb start-server
sleep "$delay"
done
}
retry adb install -r app.apk
retry adb shell am instrument -w -r ...
对于 am instrument 重试,还应先对测试包执行 pm clear——否则瞬时状态会在多次尝试间残留。请注意,retry 只会在传输错误时重新运行(adb 主机二进制文件以非零值退出);真正的测试失败不会导致 am instrument 以非零值退出,因此不会触发重试——应通过解析输出而非退出代码来检测此类失败。
并行扇出到多台设备
adb devices \
| tail -n +2 \
| awk '$2=="device"{print $1}' \
| xargs -I {} -P 4 adb -s {} <command>
关键片段:
tail -n +2会跳过List of devices attached标题。awk '$2=="device"'会过滤掉unauthorized、offline、recovery等状态。-P 4表示并行运行 4 个任务;请根据物理 USB 集线器的数量调整。adb -s {}用于指定序列号。$ANDROID_SERIAL是在启动的 shell 中设置的等效环境变量。
对于共享设备状态的测试(清除 logcat、安装应用),先串行执行准备工作,再并行运行:
SERIALS=$(adb devices | awk '$2=="device"{print $1}')
echo "$SERIALS" | xargs -I {} -P 0 adb -s {} install -r app-debug.apk
echo "$SERIALS" | xargs -I {} -P 0 adb -s {} install -r app-debug-androidTest.apk
# Distribute shards across devices (one shard per device)
i=0
for S in $SERIALS; do
adb -s "$S" logcat -c
adb -s "$S" shell am instrument -w -r \
-e numShards $(echo "$SERIALS" | wc -l) -e shardIndex "$i" \
com.example.test/androidx.test.runner.AndroidJUnitRunner \
> "results-$S.txt" &
i=$((i+1))
done
wait
# am instrument always exits 0 — derive pass/fail from the captured output:
if grep -qE '^INSTRUMENTATION_STATUS_CODE: -[12]$' results-*.txt; then
echo "test failures detected"; exit 1
fi
(xargs -P 0 会根据输入项数量尽可能多地并发运行。)
通过 numShards / shardIndex 进行分片
“如果需要并行执行测试……请使用
-e numShards选项指定要创建的独立分片数量,并使用-e shardIndex选项指定要运行哪个分片。”——AndroidJUnitRunner 文档。
测试按名称进行哈希分桶:在测试集相同的情况下,各次运行的结果是确定的。添加或移除测试会重新分配分桶。
按设备区分产物路径
始终使用序列号作为命名空间:
artifact_dir() { echo "artifacts/${1//[:_]/-}"; }
for S in $(adb devices | awk '$2=="device"{print $1}'); do
D=$(artifact_dir "$S"); mkdir -p "$D"
adb -s "$S" logcat -d -v threadtime > "$D/logcat.txt"
adb -s "$S" shell screencap -p /sdcard/last.png
adb -s "$S" pull /sdcard/last.png "$D/last.png"
done
使用 trap 实现幂等的设置与清理
切勿假设上次运行后状态是干净的。将清理逻辑纳入设置流程,并使用 trap 确保完成拆卸。
最简设置前置代码
adb wait-for-device
adb shell input keyevent 82 # wake; no-op if already on
adb shell pm clear com.example.app # wipe app data + cache
adb shell pm clear com.example.app.test # wipe test process state
adb shell am force-stop com.example.app
# Hermetic animations
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
(有关 pm clear 与 am force-stop 的完整说明,请参阅 ../../control/injecting-input-and-state/SKILL.md。)
使用 trap 清理
cleanup() {
adb shell am force-stop com.example.app || true
adb shell pm clear com.example.app || true
adb uninstall com.example.app || true
adb forward --remove tcp:6100 || true
adb reverse --remove-all || true
adb shell settings put global window_animation_scale 1 || true
adb shell settings put global transition_animation_scale 1 || true
adb shell settings put global animator_duration_scale 1 || true
}
trap cleanup EXIT
每个步骤都使用 || true——清理部分状态时绝不能掩盖原始失败退出码。
失败时捕获信息
set +e
./gradlew connectedDebugAndroidTest
RC=$?
set -e
if [ "$RC" -ne 0 ]; then
mkdir -p artifacts
adb shell screencap -p /sdcard/fail.png
adb pull /sdcard/fail.png artifacts/fail.png
adb logcat -d -v threadtime > artifacts/logcat.txt
adb logcat -d -b crash -v threadtime > artifacts/crash.txt
adb logcat -d -b events -v descriptive > artifacts/events.txt
adb shell dumpsys activity > artifacts/dumpsys-activity.txt
fi
exit $RC
(有关底层原语,请参阅 ../../capture/capturing-screenshots-and-screenrecord/SKILL.md 和 ../../observability/extracting-logs-with-logcat/SKILL.md。)
配置 Test Orchestrator
“Android Test Orchestrator 会在测试套件运行开始时收集 JUnit 测试,但随后会在独立的
Instrumentation实例中分别执行每项测试。”——developer.android.com。
每个 @Test 都会在独立的 Instrumentation 调用中运行,因此各项测试之间的进程状态、静态变量和 Application 单例都会重置。
Gradle 配置——使用 androidTestUtil,而非 androidTestImplementation
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}
dependencies {
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestUtil 'androidx.test:orchestrator:1.6.1' // NOT androidTestImplementation
}
clearPackageData: 'true' 会让 Orchestrator 在测试之间运行 pm clear <pkg>,这是最彻底的隔离选项。
直接调用 ADB(非 Gradle)
adb install --force-queryable -r path/to/orchestrator-1.6.1.apk
adb install --force-queryable -r path/to/test-services-1.6.0.apk
adb shell 'CLASSPATH=$(pm path androidx.test.services) app_process / \
androidx.test.services.shellexecutor.ShellMain am instrument -w -e \
targetInstrumentation com.example.test/androidx.test.runner.AndroidJUnitRunner \
androidx.test.orchestrator/.AndroidTestOrchestrator'
API 30+ 需要使用 --force-queryable,这样 Orchestrator 才能在遵循软件包可见性规则的情况下,通过 bindService 连接到目标应用。
权衡
- 优点: 每项测试都从状态已知且正常的
Application开始;一项测试崩溃不会导致其余测试无法运行;测试之间的pm clear是真正执行的,而不是“我在@After里清理了”。 - 缺点: 每项测试启动进程会增加约 1–2 秒。一个包含 500 项测试的套件,耗时会从约 5 分钟增加到 15 分钟以上。仅在状态泄漏确实造成问题的集成测试套件中使用。
模式
模式:错误——依据 am instrument 的退出码判断 CI 是否通过
# WRONG
adb shell am instrument -w -r com.example.test/androidx.test.runner.AndroidJUnitRunner
RC=$?
[ "$RC" -eq 0 ] && echo "tests passed" || echo "tests failed (rc=$RC)"
# WRONG because: am instrument calls System.exit(0) (AOSP Instrument.java) and AndroidJUnitRunner's
# finish() reports RESULT_OK regardless. $? is 0 even when tests fail — with OR without -w.
# RIGHT — keep -w (so stdout is complete) and -r (raw, parseable), then parse the stream
adb shell am instrument -w -r com.example.test/androidx.test.runner.AndroidJUnitRunner \
| tee instrument.log
# -1 = error, -2 = failure; -3 (ignored) and -4 (assumption failure) are skips, not failures.
if grep -qE '^INSTRUMENTATION_STATUS_CODE: -[12]$' instrument.log; then
echo "tests failed"; exit 1
fi
echo "tests passed"
模式:错误——混淆 adb forward 和 adb reverse 的参数顺序
# WRONG — copy/pasted the forward syntax for reverse
adb reverse tcp:8080 tcp:8080 # ambiguous (works because both are tcp:8080)
adb reverse tcp:9000 tcp:8080 # this means: device port 9000 → host port 8080
# Developer expected: host service on 8080 reachable as 9000 on device.
# WRONG because: reverse argument order is REMOTE LOCAL. The device tries to dial 8080 on
# the host, but the dev meant to expose the local 8080 to the device on port 9000.
# RIGHT — adb reverse REMOTE LOCAL
# Expose host service on 8080 to the device as 9000:
adb reverse tcp:9000 tcp:8080
# In the app code: open a connection to localhost:9000 — it terminates at the host's :8080.
# Or, simpler, identical port:
adb reverse tcp:8080 tcp:8080 # device 8080 ⇆ host 8080
模式:错误——为 orchestrator APK 使用 androidTestImplementation
// WRONG
dependencies {
androidTestImplementation 'androidx.test:orchestrator:1.6.1'
}
// WRONG because: the orchestrator is a SEPARATE APK, not a library. androidTestImplementation
// merges it into the test APK's classpath, which AGP rejects (or silently ignores). The
// correct configuration is androidTestUtil, which AGP installs as an additional APK.
// RIGHT
dependencies {
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestUtil 'androidx.test:orchestrator:1.6.1'
}
模式:为 instrumentation 调用设置超时
timeout 600s adb shell am instrument -w -r \
com.example.test/androidx.test.runner.AndroidJUnitRunner | tee instrument.log
RC=${PIPESTATUS[0]} # exit of `timeout`, not `tee`
case "$RC" in
124) echo "timed out at 600s"; exit 124 ;;
137) echo "timed out and had to be SIGKILLed"; exit 137 ;;
esac
# RC is 0 whether tests passed or failed (am instrument always exits 0); pass/fail comes from the stream:
if grep -qE '^INSTRUMENTATION_STATUS_CODE: -[12]$' instrument.log; then
echo "failed"; exit 1
fi
echo "passed"
模式:失败后重启服务再重试
retry adb -s "$SERIAL" install -r app-debug.apk
retry adb -s "$SERIAL" shell am instrument -w -r \
-e clearPackageData true \
com.example.test/androidx.test.runner.AndroidJUnitRunner
retry 辅助函数会在发生瞬时故障时重启 adb 服务(参见上文“瞬时错误时重试”)。
模式:并行分片分发,并按设备区分产物
SERIALS=( $(adb devices | awk '$2=="device"{print $1}') )
N=${#SERIALS[@]}
mkdir -p artifacts
pids=()
for i in "${!SERIALS[@]}"; do
S="${SERIALS[$i]}"
D="artifacts/${S//[:_]/-}"
mkdir -p "$D"
(
adb -s "$S" logcat -c
adb -s "$S" shell pm clear com.example.app
adb -s "$S" shell am instrument -w -r \
-e numShards "$N" -e shardIndex "$i" \
com.example.test/androidx.test.runner.AndroidJUnitRunner \
> "$D/results.txt"
# am instrument exits 0 regardless — pass/fail is in the captured stream.
if grep -qE '^INSTRUMENTATION_STATUS_CODE: -[12]$' "$D/results.txt"; then
adb -s "$S" shell screencap -p /sdcard/fail.png
adb -s "$S" pull /sdcard/fail.png "$D/fail.png"
adb -s "$S" logcat -d -v threadtime > "$D/logcat.txt"
exit 1
fi
) &
pids+=($!)
done
rc=0
for p in "${pids[@]}"; do wait "$p" || rc=1; done # fail the job if any shard failed
exit $rc
强制规则
- 必须向
am instrument传入-w -r,这样 shell 才会等待运行器并输出原始、可解析的结果。不加-w时,shell 会在产生任何输出之前返回。 - 禁止根据
am instrument的$?判断 CI 是否通过——无论测试结果如何(无论是否加-w),它都会调用System.exit(0)。应通过在标准输出中搜索INSTRUMENTATION_STATUS_CODE: -1(错误)和-2(失败)来检测失败。 - 必须在解析
INSTRUMENTATION_STATUS_CODE时,将-3(已忽略)和-4(假设失败)与实际失败区分开来。-1和-2才是失败代码。 - 必须记住,
adb forward的参数顺序是LOCAL REMOTE(主机在前),而adb reverse的顺序是REMOTE LOCAL(设备在前)。两者的参数顺序相反。 - 必须使用
timeout <N>s(macOS 上使用gtimeout)包装长时间运行的 adb 命令。adb没有内置的超时选项——adb -t指的是传输 ID,不是超时。 - 必须使用
androidTestUtil("androidx.test:orchestrator:1.6.1"),不要使用androidTestImplementation。Orchestrator 是独立 APK,不是库。 - 必须使用有界的
retry辅助函数重试临时 adb 错误(device not found、closed、protocol fault),并在重试之间调用adb kill-server; adb start-server。 - 必须安装
trap cleanup EXIT,以清理端口转发(adb forward --remove-all、adb reverse --remove-all)、恢复动画缩放比例,并对待测应用执行pm clear。每个步骤都要加上|| true,避免清理操作掩盖测试的退出代码。 - 每次测试运行前都必须执行
pm clear <pkg>(不能只用am force-stop)。参见../../control/injecting-input-and-state/SKILL.md。 - 禁止依赖 API 24 以下设备上的
adb shell退出代码传递。对于较旧的目标设备,应在标准输出中使用哨兵值__ADB_RC=$?,或为测试基础设施指定最低 SDK 版本。 - 禁止将
adb -t <transport-id>与timeout混淆。两者毫无相同之处。 - 禁止在未串行化安装/设置阶段的情况下并行执行——同时向同一设备执行
adb install会产生竞争。 - 优先使用: Test Orchestrator 搭配
clearPackageData: 'true'实现隔离性测试;对于经过路由的产物,再搭配useTestStorageService: 'true'(参见../../transfer/extracting-test-artifacts/SKILL.md)。 - 优先采用: 按序列号为产物划分命名空间(
artifacts/${SERIAL//[:_]/-}/),这样 CI 中的archive artifacts/**/*就能整洁地收集所有产物。
验证
- 脚本/Gradle 中的每次
am instrument调用都带有-w -r,且没有脚本将其$?用作通过/失败信号——应通过解析标准输出中的INSTRUMENTATION_STATUS_CODE: -1/-2来检测失败。 -
adb forward和adb reverse的参数顺序均符合文档说明;CI 会运行冒烟测试,确认主机能访问设备服务,反之亦然。 - 使用
androidTestUtil('androidx.test:orchestrator:...')(而非androidTestImplementation)——./gradlew :app:dependencies --configuration androidTestUtil会显示 Orchestrator。 - 顶层 CI 脚本中已安装
trap cleanup EXIT。 - 所有长时间运行的 adb 命令都已使用
timeout包装。 - CI 运行完成 50 次迭代,且未出现“端口转发未清理”或“上次运行的动画缩放比例”导致的间歇性故障。
- 状态代码解析会将
-3和-4视为跳过,而非失败。 - 按设备划分的产物目录均以序列号作为命名空间;CI 产物归档中显示的目录结构符合预期。
- 遇到临时错误时,重试辅助函数会在重试之间执行
adb kill-server; adb start-server。
参考资料
- developer.android.com/tools/adb —
forward/reverse语法、传输和服务器。 - developer.android.com/studio/test/command-line —
am instrument -w -r、-e numShards/-e shardIndex、AndroidJUnitRunner 参数、输出目录。 - developer.android.com/training/testing/instrumented-tests — Test Orchestrator 概述。
- developer.android.com/reference/android/app/Instrumentation —
REPORT_VALUE_RESULT_*常量(OK = 0、START = 1、ERROR = -1、FAILURE = -2)。 - firebase.google.com/docs/test-lab/android/command-line — Firebase Test Lab CI 退出代码(
0通过、10测试失败、15状态不确定、18不兼容、20基础设施错误)。 - 研究说明
tasks/research/A3-adb-observability-automation.md— 完整的 bash 惯用写法、退出代码传播矩阵、端口转发令牌语法、并行扇出、重试、失败时捕获、Test Orchestrator 接线。 - 研究说明
tasks/research/A2-adb-shell-commands.md—am instrument参数、状态代码、AndroidJUnitRunner 的-3/-4扩展。 - 同级技能:
../../architecture/understanding-adb-architecture/SKILL.md— 服务器 / 守护进程 /adb kill-server。 - 同级技能:
../../devices/connecting-to-devices/SKILL.md— 设备状态、wait-for-device。 - 同级技能:
../../devices/connecting-over-wifi/SKILL.md— 用于无头 CI 的adb pair/adb connect。 - 同级技能:
../../apps/installing-and-managing-apps/SKILL.md—pm install/pm uninstall/pm clear。 - 同级技能:
../../tests/running-instrumented-tests-via-adb/SKILL.md— 深入介绍am instrument -w -r。 - 同级技能:
../../control/injecting-input-and-state/SKILL.md— hermetic 前置步骤(将动画设为 0、pm clear、am force-stop)。 - 同级技能:
../../capture/capturing-screenshots-and-screenrecord/SKILL.md— 失败时使用screencap/screenrecord捕获。 - 同级技能:
../../observability/extracting-logs-with-logcat/SKILL.md— 失败时使用logcat -d捕获。 - 同级技能:
../../transfer/extracting-test-artifacts/SKILL.md—pull/push/run-as/TestStorage。 - 跨集合:
../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md— Runner 内部机制。 - 跨集合:
../../../instrumentation/scenarios/launching-activities-with-activityscenario/SKILL.md— Instrumentation 下的 Activity 场景。 - 跨集合:
../../../fundamentals/strategies/applying-testing-strategies/SKILL.md— 何时应投入 CI 编排,而非使用更简单的工具。