mcpField
E
extracting-test-artifacts

extracting-test-artifacts

github.com/skydoves测试与质量33964 次安装22 周前更新
代码仓库

关于 extracting-test-artifacts

面向需要在主机与设备间传输测试文件的 Android 开发者,介绍 adb 权限规则、使用 run-as 安全提取二进制文件,以及通过 AndroidX TestStorage 收集单项测试产物。

主要功能

  1. 01介绍 adb push 与 pull 用法
  2. 02说明设备路径的访问权限
  3. 03使用 run-as 访问应用私有文件
  4. 04通过 exec-out 二进制安全提取
  5. 05介绍 AndroidX TestStorage 产物收集

适用场景

  1. 01从可调试测试应用提取数据库
  2. 02将截图或 JSON 文件保存为 CI 产物
  3. 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 TestStorage API。

何时不使用本技能

  • 捕获作为产物的屏幕截图或视频。请使用 ../../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 无法完成的三件事
  1. 在发布版本/不可调试版本上运行。会打印“Package is not debuggable”。
  2. 切换到系统 UID(只能切换到目标软件包的 UID)。
  3. 单独跨越用户配置文件边界。如有需要,请使用 --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-*.xmlJUnit 风格的 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 — 何时适合将产物用作可观测性层。

评论

支持 Markdown