关于 extracting-test-artifacts
面向需要在主机与设备间传输测试文件的 Android 开发者,介绍 adb 权限规则、使用 run-as 安全提取二进制文件,以及通过 AndroidX TestStorage 收集单项测试产物。
主要功能
- 01介绍 adb push 与 pull 用法
- 02说明设备路径的访问权限
- 03使用 run-as 访问应用私有文件
- 04通过 exec-out 二进制安全提取
- 05介绍 AndroidX TestStorage 产物收集
适用场景
- 01从可调试测试应用提取数据库
- 02将截图或 JSON 文件保存为 CI 产物
- 03向设备传输大型测试数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill extracting-test-artifacts安装后在 Claude Code 中输入/extracting-test-artifacts确认已加载。
详细介绍
提取测试产物 — adb pull / push 和 run-as
本技能介绍如何在主机与设备之间移动文件、决定 pull 是否成功的路径权限规则,以及用于按测试路由产物的 androidx.test:services TestStorage API。失败时的捕获编排(何时拉取哪些内容)请参阅 ../../automation/scripting-adb-for-ci/SKILL.md。
何时使用本技能
- 测试会生成需要作为构建产物保存到 CI 的产物(屏幕截图、JSON、捕获的数据库)。
adb pull /data/data/com.example/databases/main.db失败并提示“Permission denied”;开发者不知道run-as的存在。- 二进制文件(PNG、MP4、SQLite 数据库)传到主机后字节错乱——解决方法是使用
exec-out。 - 推送较大的 APK 或测试数据文件很慢;开发者想使用现代压缩标志(
-z brotli)。 - 测试应用会将屏幕截图写入
/sdcard/Android/data/com.example/files/screenshots/,开发者想知道 API 30 及更高版本是否可以拉取这些文件(可以,只要该包拥有这些文件)。 - 团队正从“写入 /sdcard,再在主机上拉取”迁移到现代 AndroidX
TestStorageAPI。
何时不使用本技能
- 捕获作为产物的屏幕截图或视频。请使用
../../capture/capturing-screenshots-and-screenrecord/SKILL.md。 - 捕获要提取的日志。请使用
../../observability/extracting-logs-with-logcat/SKILL.md。 - 安装或卸载 APK。请使用
../../apps/installing-and-managing-apps/SKILL.md。 - 操控设备(点击、按键事件)。请使用
../../control/injecting-input-and-state/SKILL.md。 - 整体 CI 编排(何时拉取、重试、并行化)。请使用
../../automation/scripting-adb-for-ci/SKILL.md。
前置条件
adb get-state返回device。请参阅../../devices/connecting-to-devices/SKILL.md。- 使用
run-as时:目标 APK 的合并清单中必须设置android:debuggable="true"(默认的debug构建变体)。 - 使用
-z brotli/zstd时:主机上需要 platform-tools 30+,设备上的 adbd 需要 ≥ Android 11。 - 使用
TestStorage时:需要androidx.test:services,并在am instrument中传入-e useTestStorageService true参数。
对称语法
“与
install命令不同,install只会将 APK 文件复制到指定位置,而pull和push命令可以让你将任意目录和文件复制到设备上的任意位置。” — developer.android.com/tools/adb
adb pull <remote> [<local>] # device → host (defaults to cwd)
adb push <local> <remote> # host → device
现代标志(platform-tools ≥ 30)
push [--sync] [-z ALGORITHM] [-Z] LOCAL... REMOTE
pull [-a] [-z ALGORITHM] [-Z] REMOTE... LOCAL
| 标志 | 含义 |
|---|---|
-z <algo> | 启用压缩。<algo> ∈ any / none / brotli / lz4 / zstd。any 表示由 adbd 选择。 |
-Z | 禁用压缩。 |
--sync(仅限 push) | 仅推送时间戳不同的文件,实现快速增量更新。 |
-a(仅限 pull) | 保留文件时间戳和模式。 |
-n | 试运行。 |
-q | 不显示进度消息。 |
旧版 adb pull -p(显示进度)现在不起作用——现代 adb 默认会显示进度,使用 -q 可将其关闭。不要在 CI 脚本中依赖 -p。
路径权限规则
这是最关键的表格。大多数 adb pull 的“Permission denied”错误都源于对这些规则的误解。
| 路径 | adb shell 可写? | pull 是否可用? |
|---|---|---|
/data/local/tmp/ | 是 | 是 |
/sdcard/(= /storage/emulated/0/) | 是(API 30 及更高版本受分区存储规则约束) | 是 |
/data/data/<pkg>/ | 仅可通过 run-as <pkg> 访问(可调试构建) | 仅可通过 run-as |
/sdcard/Android/data/<pkg>/files/ | 由包拥有,受分区存储规则约束 | 对拥有该目录的包可用,对 adb shell 也可用(FUSE/sdcardfs 桥接) |
/data/local/tmp/
由为 adb 提供支持的 shell 用户读写。无需进行应用权限配置。将其用作所有内容的暂存目录:推送的 APK、测试数据、捕获的屏幕截图、JSON 输入、已 dex 化的插桩 JAR。
注意:应用以 u0_aXX 身份运行,而不是以 shell 身份运行,因此除非执行 chmod 0644 且父目录允许所有用户读取,否则应用无法读取由 shell 写入的文件。/data/local/tmp/ 本身的权限为 0771,因此推送应用必须读取的文件时请务必谨慎。
/sdcard/
/storage/emulated/0/ 的别名,有时也会是 /storage/self/primary/。公共/外部存储。在 API 30 及更高版本上,ADB 的 shell 用户仍可通过 FUSE/sdcardfs 访问,因此即使分区存储限制收紧后,测试中的 adb push /sdcard/... 和 adb pull /sdcard/... 仍可正常工作。在 API 30 及更高版本上,应用侧写入受 MediaStore 管理。
/data/data/<pkg>/ — 仅限 run-as
由应用的 UID 所有;目录本身权限为 0700。在生产用户版本上,直接执行 adb pull /data/data/com.example/... 会失败。有两种可行方式:
- 在可调试版本上使用
run-as <pkg>(最常见)。 - 在
userdebug/eng版本上使用adb root(详见../../automation/scripting-adb-for-ci/SKILL.md)。
/sdcard/Android/data/<pkg>/files/
应用的“外部私有”存储——应用通过 Context.getExternalFilesDir(null) 写入。由于该路径位于由 FUSE 模拟的 SD 卡下,因此可通过 adb shell 读取。对于 Android 11 及更高版本上的“写入构件,然后从 CI 拉取”,这是最省事的路径:
# Inside the test app:
val out = File(context.getExternalFilesDir(null), "screenshots/fail.png")
# From CI:
adb pull /sdcard/Android/data/com.example/files/screenshots/fail.png ./
在可调试版本上使用 run-as
run-as <pkg> 是从 Gingerbread 起内置于 Android 的 setuid 辅助程序。它会将 shell 切换为应用的 UID,前提是且仅当前提是 APK 的合并清单中包含 android:debuggable="true"(debug 构建变体的默认设置)。用法:adb shell run-as <pkg> <cmd>。
获取文本文件
adb shell run-as com.example.app cat databases/main.db > main.db
这适用于较短的文本文件。对于二进制文件,PTY 转换可能会损坏字节(见下文的 exec-out)。
通过 exec-out + tar 无损获取二进制文件
对于目录、二进制文件或字节中包含 \n 的任何内容,必须使用 exec-out(二进制无损 stdout)并通过管道传递给 tar:
adb exec-out run-as com.example.app tar cf - databases/ \
| tar xf - -C ./pulled
tar cf - 会在 stdout 上生成归档;本地的 tar xf - 会将其解压到 ./pulled/。无需中间文件,不会进行 PTY 转换,适用于任意二进制内容。
获取单个二进制文件
adb exec-out run-as com.example.app cat databases/main.db > main.db
exec-out 会保持 stdout 的二进制数据无损。
列出内容
adb shell run-as com.example.app ls -lR files/
run-as 无法完成的三件事
- 在发布版本/不可调试版本上运行。会打印“Package is not debuggable”。
- 切换到系统 UID(只能切换到目标软件包的 UID)。
- 单独跨越用户配置文件边界。如有需要,请使用
--user N。
常见测试构件路径
| 路径 | 来源 |
|---|---|
app/build/outputs/connected_android_test_additional_output/<flavor><Variant>/connected/<deviceId>/ | Gradle 的标准“附加输出”目录。插桩运行器会拉取通过 androidx.test:runner 参数 additionalTestOutputDir 声明的所有内容。 |
app/build/outputs/androidTest-results/connected/<flavor><Variant>/TEST-*.xml | JUnit 风格的 XML 测试结果——可直接由 Jenkins/GHA 等工具读取。 |
app/build/reports/androidTests/connected/ | HTML 测试报告。 |
(参见 developer.android.com/studio/test/command-line:HTML 位于 module/build/reports/androidTests/connected/,XML 位于 module/build/outputs/androidTest-results/connected/。)
现代构件 API — TestStorage
旧模式是“写入 /sdcard/...,再通过 CI 脚本拉取”。现代模式通过 androidx.test.services.storage.TestStorage 和 useTestStorageService 标志进行路由。
配置
// dependencies
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestUtil("androidx.test.services:test-services:1.6.0")
// am instrument args (or Gradle: testInstrumentationRunnerArguments)
adb shell am instrument -w -r \
-e useTestStorageService true \
com.example.test/androidx.test.runner.AndroidJUnitRunner
测试中的用法
val storage = androidx.test.platform.io.PlatformTestStorageRegistry.getInstance()
storage.openOutputFile("screenshots/fail-${test.methodName}.png").use { out ->
out.write(pngBytes)
}
TestStorage 解决的问题
- 输出文件会存放在由 TestStorageService 管理的目录中,而不是直接存放在
/sdcard/下。 - Gradle 的
connectedAndroidTest会自动将这些文件拉取到app/build/outputs/connected_android_test_additional_output/。 - 按测试隔离文件名由运行器处理,而不是通过手动编写的
Class.method文件名转换逻辑处理。 - 与 Test Orchestrator 的
clearPackageData: 'true'兼容——即使测试应用在测试之间被清除,存储服务仍会保留文件。
对于非 Gradle 流水线,存储会使用相同的 FS 布局,仍可通过 /sdcard/googletest/test_outputfiles/... 访问,以保持旧版兼容性。
使用模式
模式:错误做法——在可调试构建上使用 adb pull /data/data/<pkg>/...
# WRONG
adb pull /data/data/com.example/databases/main.db
# adb: error: failed to stat remote object '/data/data/com.example/databases/main.db': Permission denied
# WRONG because: /data/data/<pkg>/ is owned by the app UID with mode 0700. The shell user
# cannot traverse it. run-as solves this on debuggable builds.
# RIGHT — run-as + exec-out + cat for a binary-clean grab
adb exec-out run-as com.example cat databases/main.db > main.db
file main.db # → "SQLite 3.x database"
对于目录:
adb exec-out run-as com.example tar cf - databases/ | tar xf - -C ./pulled
模式:错误做法——对二进制文件使用 adb shell run-as <pkg> cat <binary>
# WRONG (on Windows / certain CI hosts)
adb shell run-as com.example cat databases/main.db > main.db
# WRONG because: `adb shell` allocates a PTY and translates LF to CRLF on stdout. SQLite
# headers and other binary content get corrupted. The pulled DB fails to open.
# RIGHT — exec-out is binary-clean
adb exec-out run-as com.example cat databases/main.db > main.db
模式:在应用内写入文件,由 CI 拉取
// In production / test code
val out = File(context.getExternalFilesDir(null), "captures/${name}.png")
out.outputStream().use { it.write(bitmapBytes) }
# CI script
adb pull /sdcard/Android/data/com.example/files/captures/ ./artifacts/
在 API 30 及更高版本中,无需 run-as 即可使用此方式,因为 /sdcard/Android/data/<pkg>/files/ 是该软件包专属的外部私有目录。应用和 adb shell(FUSE 桥接)均可读取该目录。
模式:快速增量推送大型测试数据
adb push --sync -z brotli ./fixtures/ /sdcard/fixtures/
--sync 会跳过时间戳匹配的文件;-z brotli 会压缩传输数据。对于包含数百个未更改文件的 fixtures 目录,第二次推送不会传输任何内容。
模式:使用 TestStorage 在失败时捕获
class CaptureOnFailureRule : TestWatcher() {
override fun failed(e: Throwable, description: Description) {
val storage = PlatformTestStorageRegistry.getInstance()
val name = "${description.className}.${description.methodName}"
storage.openOutputFile("captures/$name.png").use { out ->
out.write(captureScreenshotPng())
}
}
}
运行 connectedAndroidTest 后,文件会自动存放到 app/build/outputs/connected_android_test_additional_output/<flavor><Variant>/connected/<device>/captures/<name>.png。
强制规则
- 必须使用
adb exec-out run-as <pkg> cat <path>(而不是adb shell run-as ...)提取二进制文件。在 Windows 和某些 CI 主机上,PTY 转换会损坏二进制数据。 - 必须使用
adb exec-out run-as <pkg> tar cf - <dir> | tar xf -提取/data/data/<pkg>/下的目录。没有可通过run-as使用的等效adb pull命令。 - 必须在编写
run-as脚本前,确认构建是可调试的。在发布版 APK 上,run-as会以非零状态码退出,并显示“Package is not debuggable”。 - 必须在 API 30 及更高版本中,优先使用
/sdcard/Android/data/<pkg>/files/(通过Context.getExternalFilesDir(null)获取),而不是任意的/sdcard/<dir>/路径来存放应用写入的测试文件。后者的应用写入操作受 MediaStore 限制。 - 对于新测试基础设施,必须优先使用
TestStorage(-e useTestStorageService true+androidx.test:services),而不是自行编写/sdcard/...路径。它会按测试隔离文件,并与 Gradle 的connected_android_test_additional_output/输出目录集成。 - 不得依赖
adb pull -p(显示进度)——它在 platform-tools 30 及更高版本中已是旧版空操作。请使用默认进度指示器(或使用-q将其静默)。 - 不得假设不使用
run-as就能通过adb pull拉取/data/data/<pkg>/...。即使在可调试构建上也不行——pull操作使用的是shell用户。 - **推荐:**重复推送大型 fixture 集时使用
--sync -z brotli。Brotli 压缩与跳过匹配时间戳相结合,在速度较慢的 USB 2.0 数据线上能显著提速。 - **推荐:**结合使用 Test Orchestrator、
clearPackageData: 'true'和useTestStorageService: 'true',实现具有文件保留功能的隔离式测试。
验证
- 没有脚本直接调用
adb pull /data/data/<pkg>/...。所有此类拉取都通过run-as执行。 - 拉取二进制文件时使用
adb exec-out,而不是adb shell。 - 应用写入的所有测试产物都写入
Context.getExternalFilesDir(...)(位于/sdcard/Android/data/<pkg>/files/),而不是/sdcard/<arbitrary>/。 - CI 会为现代产物 API 归档
app/build/outputs/connected_android_test_additional_output/**/*。 - 多次推送同一 fixture 目录时使用
adb push --sync,跳过未更改的文件。 -
file对每个拉取的产物都报告正确的二进制类型(没有 CRLF 损坏)。 - Release APK 中没有尝试从
/data/data/<pkg>/...执行adb pull的测试代码路径——此类操作会失败。
参考资料
- developer.android.com/tools/adb —
pull/push语法,以及“允许你将任意目录和文件复制到设备上的任意位置”这段说明。 - developer.android.com/studio/test/command-line — Gradle 输出目录约定:HTML 位于
module/build/reports/androidTests/connected/,XML 位于module/build/outputs/androidTest-results/connected/。 - developer.android.com/training/data-storage/app-specific —
Context.getExternalFilesDir()和 API 30 及以上版本的分区存储规则。 - developer.android.com/reference/androidx/test/services/storage/TestStorage — 现代产物 API。
- developer.android.com/reference/androidx/test/platform/io/PlatformTestStorageRegistry — 主机端句柄。
- 研究笔记
tasks/research/A3-adb-observability-automation.md— 完整的 pull/push 操作范围、路径权限规则、run-as 模式、Android 11 及以上版本的分区存储、TestStorage 接线方式。 - 同级技能:
../../architecture/understanding-adb-architecture/SKILL.md— server / daemon、exec-out 与 shell。 - 同级技能:
../../devices/connecting-to-devices/SKILL.md—adb get-state、多设备定向。 - 同级技能:
../../devices/connecting-over-wifi/SKILL.md— 无线传输。 - 同级技能:
../../apps/installing-and-managing-apps/SKILL.md—pm install和 debuggable-build 标志。 - 同级技能:
../../tests/running-instrumented-tests-via-adb/SKILL.md—am instrument参数,包括useTestStorageService。 - 同级技能:
../../control/injecting-input-and-state/SKILL.md— 驱动生成产物的场景。 - 同级技能:
../../capture/capturing-screenshots-and-screenrecord/SKILL.md— 生成待拉取的 PNG / MP4。 - 同级技能:
../../observability/extracting-logs-with-logcat/SKILL.md— 通过adb shell logcat -f+adb pull拉取轮转日志文件。 - 同级技能:
../../automation/scripting-adb-for-ci/SKILL.md— 完整的 CI 编排。 - 跨集合:
../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md— 接入TestStorage的TestWatcher规则。 - 跨集合:
../../../instrumentation/scenarios/launching-activities-with-activityscenario/SKILL.md— 生成产物的 Activity 驱动场景。 - 跨集合:
../../../fundamentals/strategies/applying-testing-strategies/SKILL.md— 何时适合将产物用作可观测性层。