Extracting Test Artifacts — adb pull / push and run-as
This skill covers moving files between host and device, the path permission rules that decide whether a pull succeeds, and the androidx.test:services TestStorage API for routing per-test artefacts. Capture-on-failure orchestration (when to pull what) lives in ../../automation/scripting-adb-for-ci/SKILL.md.
When to use this skill
- A test produces artefacts (screenshots, JSON, captured DBs) that need to land on CI as build outputs.
adb pull /data/data/com.example/databases/main.dbfails with "Permission denied"; the developer doesn't knowrun-asexists.- A binary file (PNG, MP4, SQLite DB) arrives on the host with mangled bytes — the missing
exec-outis the fix. - A push of a large APK or test data file is slow; the developer wants the modern compression flags (
-z brotli). - The test app writes screenshots to
/sdcard/Android/data/com.example/files/screenshots/and the developer wants to know whether that's pullable on API 30+ (it is, when the package owns it). - The team is migrating from "write to /sdcard, pull on the host" to the modern AndroidX
TestStorageAPI.
When NOT to use this skill
- Capturing the screenshot or video that becomes the artefact. Use
../../capture/capturing-screenshots-and-screenrecord/SKILL.md. - Capturing logs to extract. Use
../../observability/extracting-logs-with-logcat/SKILL.md. - Installing or uninstalling APKs. Use
../../apps/installing-and-managing-apps/SKILL.md. - Driving the device (taps, key events). Use
../../control/injecting-input-and-state/SKILL.md. - The whole CI orchestration (when to pull, retries, parallelisation). Use
../../automation/scripting-adb-for-ci/SKILL.md.
Prerequisites
adb get-statereturnsdevice. See../../devices/connecting-to-devices/SKILL.md.- For
run-as: the target APK must beandroid:debuggable="true"in its merged manifest (the defaultdebugbuild variant). - For
-z brotli/zstd: platform-tools 30+ on the host and adbd ≥ Android 11 on the device. - For
TestStorage:androidx.test:servicesand the-e useTestStorageService trueargument toam instrument.
Symmetric syntax
"Unlike the
installcommand, which only copies an APK file to a specific location, thepullandpushcommands let you copy arbitrary directories and files to any location in a device." — developer.android.com/tools/adb
adb pull <remote> [<local>] # device → host (defaults to cwd)
adb push <local> <remote> # host → device
Modern flags (platform-tools ≥ 30)
push [--sync] [-z ALGORITHM] [-Z] LOCAL... REMOTE
pull [-a] [-z ALGORITHM] [-Z] REMOTE... LOCAL
| Flag | Meaning |
|---|---|
-z <algo> |
Enable compression. <algo> ∈ any / none / brotli / lz4 / zstd. any lets adbd pick. |
-Z |
Disable compression. |
--sync (push only) |
Only push files with different timestamps — fast incremental updates. |
-a (pull only) |
Preserve file timestamp and mode. |
-n |
Dry-run. |
-q |
Suppress progress messages. |
The legacy adb pull -p (show progress) is now a no-op — modern adb shows progress by default and -q silences it. Don't rely on -p in CI scripts.
Path permission rules
This is the load-bearing table. Most "permission denied" adb pull failures come from misunderstanding it.
| Path | adb shell writable? | pull works? |
|---|---|---|
/data/local/tmp/ |
Yes | Yes |
/sdcard/ (= /storage/emulated/0/) |
Yes (scoped storage rules on API 30+) | Yes |
/data/data/<pkg>/ |
Only via run-as <pkg> (debuggable builds) |
Only via run-as |
/sdcard/Android/data/<pkg>/files/ |
Package-owned, scoped storage | Yes for the owning package, and yes for adb shell (FUSE/sdcardfs bridge) |
/data/local/tmp/
Readable/writable by the shell user that backs adb. No app-permission ceremony required. Use as the staging directory for everything: pushed APKs, test data, captured screenshots, JSON inputs, dexed instrumentation jars.
Caveat: your app runs as u0_aXX, not shell, so the app cannot read shell-written files unless you chmod 0644 and the parent is world-readable. /data/local/tmp/ itself is 0771, so be deliberate when pushing files the app must read.
/sdcard/
Alias /storage/emulated/0/, sometimes /storage/self/primary/. Public/external storage. ADB's shell user retains FUSE/sdcardfs access on API 30+, so adb push /sdcard/... and adb pull /sdcard/... work for tests even after scoped storage tightening. App-side writes are gated through MediaStore on API 30+.
/data/data/<pkg>/ — run-as only
Owned by the app's UID; 0700 on the dir itself. A bare adb pull /data/data/com.example/... fails on production user builds. Two viable paths:
run-as <pkg>on debuggable builds (most common).adb rootonuserdebug/engbuilds (covered briefly in../../automation/scripting-adb-for-ci/SKILL.md).
/sdcard/Android/data/<pkg>/files/
The app's "external private" storage — written from the app via Context.getExternalFilesDir(null). Readable from adb shell because it lives under FUSE-emulated SD card. The path of least resistance for "write artefact, pull from CI" on Android 11+:
# 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 on debuggable builds
run-as <pkg> is a setuid helper baked into Android since Gingerbread. It drops shell into the app's UID if and only if the APK has android:debuggable="true" in its merged manifest (the default for the debug build variant). Usage: adb shell run-as <pkg> <cmd>.
Text-file grab
adb shell run-as com.example.app cat databases/main.db > main.db
This works for short text files. For binary files it can corrupt bytes via PTY translation (see exec-out below).
Binary-clean grab via exec-out + tar
For directories, binary files, or anything with \n in the bytes, MUST use exec-out (binary-clean stdout) and pipe through tar:
adb exec-out run-as com.example.app tar cf - databases/ \
| tar xf - -C ./pulled
The tar cf - produces an archive on stdout; the local tar xf - extracts it into ./pulled/. No intermediate file, no PTY translation, works for arbitrary binary content.
Single-file binary grab
adb exec-out run-as com.example.app cat databases/main.db > main.db
exec-out keeps stdout binary-clean.
Listing
adb shell run-as com.example.app ls -lR files/
Three things run-as cannot do
- Work on release / non-debuggable builds. Will print "Package is not debuggable".
- Switch to a system UID (only the target package's UID).
- Cross profile boundaries by itself. Use
--user Nif needed.
Common test-artefact paths
| Path | Origin |
|---|---|
app/build/outputs/connected_android_test_additional_output/<flavor><Variant>/connected/<deviceId>/ |
Gradle's standard "additional output" sink. The instrumentation runner pulls anything declared via androidx.test:runner arg additionalTestOutputDir. |
app/build/outputs/androidTest-results/connected/<flavor><Variant>/TEST-*.xml |
JUnit-style XML test results — ingested directly by Jenkins/GHA/etc. |
app/build/reports/androidTests/connected/ |
HTML test report. |
(See developer.android.com/studio/test/command-line: HTML at module/build/reports/androidTests/connected/, XML at module/build/outputs/androidTest-results/connected/.)
Modern artefact API — TestStorage
The legacy pattern is "write to /sdcard/..., pull from CI script". The modern pattern routes through androidx.test.services.storage.TestStorage and a useTestStorageService flag.
Wiring
// 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
In-test usage
val storage = androidx.test.platform.io.PlatformTestStorageRegistry.getInstance()
storage.openOutputFile("screenshots/fail-${test.methodName}.png").use { out ->
out.write(pngBytes)
}
What TestStorage solves
- Output files land in a TestStorageService-managed directory rather than
/sdcard/proper. - Gradle's
connectedAndroidTestautomatically pulls them intoapp/build/outputs/connected_android_test_additional_output/. - Per-test artefact namespacing happens through the runner, not via hand-rolled
Class.methodfilename mangling. - Compatible with Test Orchestrator's
clearPackageData: 'true'— the storage service holds artefacts even though the test app is wiped between tests.
For non-Gradle pipelines, the storage backs onto the same FS layout, addressable as /sdcard/googletest/test_outputfiles/... for legacy compatibility.
Patterns
Pattern: WRONG — adb pull /data/data/<pkg>/... on a debuggable build
# 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"
Or for a directory:
adb exec-out run-as com.example tar cf - databases/ | tar xf - -C ./pulled
Pattern: WRONG — adb shell run-as <pkg> cat <binary> for binary files
# 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
Pattern: writing artefacts in-app, pulling from 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/
This works on API 30+ without run-as because /sdcard/Android/data/<pkg>/files/ is the package's own external-private dir. Both the app and adb shell (FUSE bridge) can read it.
Pattern: fast incremental push of large test data
adb push --sync -z brotli ./fixtures/ /sdcard/fixtures/
--sync skips files with matching timestamps; -z brotli compresses the wire bytes. For a fixtures dir with hundreds of unchanged files, the second push transfers nothing.
Pattern: capture-on-failure with 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())
}
}
}
After connectedAndroidTest, the artefact lands in app/build/outputs/connected_android_test_additional_output/<flavor><Variant>/connected/<device>/captures/<name>.png automatically.
Mandatory rules
- MUST use
adb exec-out run-as <pkg> cat <path>(notadb shell run-as ...) for binary file grabs. PTY translation corrupts binary on Windows and some CI hosts. - MUST use
adb exec-out run-as <pkg> tar cf - <dir> | tar xf -for directory grabs from/data/data/<pkg>/. There is noadb pullequivalent that works throughrun-as. - MUST verify the build is debuggable before scripting
run-as. On a release APK,run-asexits non-zero with "Package is not debuggable". - MUST prefer
/sdcard/Android/data/<pkg>/files/(viaContext.getExternalFilesDir(null)) over arbitrary/sdcard/<dir>/for app-written test artefacts on API 30+. The latter is gated by MediaStore for app writes. - MUST prefer
TestStorage(-e useTestStorageService true+androidx.test:services) over hand-rolled/sdcard/...paths for new test infrastructure. It namespaces per-test artefacts and integrates with Gradle'sconnected_android_test_additional_output/sink. - MUST NOT rely on
adb pull -p(show progress) — it is a legacy no-op on platform-tools 30+. Use the default progress meter (or-qto silence). - MUST NOT assume
adb pullof/data/data/<pkg>/...works withoutrun-as. It does not, even on debuggable builds —pullis ashell-user operation. - PREFERRED:
--sync -z brotlifor repeated pushes of large fixture sets. Brotli + timestamp-skip is dramatically faster on slow USB 2.0 cables. - PREFERRED: Test Orchestrator +
clearPackageData: 'true'+useTestStorageService: 'true'for hermetic test isolation with retained artefacts.
Verification
- No script invokes
adb pull /data/data/<pkg>/...directly. All such pulls go throughrun-as. - Binary-file pulls use
adb exec-out, notadb shell. - All app-written test artefacts target
Context.getExternalFilesDir(...)(lands at/sdcard/Android/data/<pkg>/files/), not/sdcard/<arbitrary>/. - CI archives
app/build/outputs/connected_android_test_additional_output/**/*for the modern artefact API. - Repeated pushes of the same fixture dir use
adb push --syncso unchanged files are skipped. -
filereports the correct binary type for every pulled artefact (no CRLF corruption). - Release APKs have no test code paths that try to
adb pullfrom/data/data/<pkg>/...— those would fail.
References
- developer.android.com/tools/adb —
pull/pushsyntax, "let you copy arbitrary directories and files to any location in a device" quote. - developer.android.com/studio/test/command-line — Gradle output dir conventions: HTML at
module/build/reports/androidTests/connected/, XML atmodule/build/outputs/androidTest-results/connected/. - developer.android.com/training/data-storage/app-specific —
Context.getExternalFilesDir()and the API-30+ scoped-storage rules. - developer.android.com/reference/androidx/test/services/storage/TestStorage — the modern artefact API.
- developer.android.com/reference/androidx/test/platform/io/PlatformTestStorageRegistry — host-side handle.
- Research note
tasks/research/A3-adb-observability-automation.md— full pull/push surface, path permission rules, run-as patterns, scoped storage on Android 11+, TestStorage wiring. - Sibling skill:
../../architecture/understanding-adb-architecture/SKILL.md— server / daemon, exec-out vs shell. - Sibling skill:
../../devices/connecting-to-devices/SKILL.md—adb get-state, multi-device targeting. - Sibling skill:
../../devices/connecting-over-wifi/SKILL.md— wireless transport. - Sibling skill:
../../apps/installing-and-managing-apps/SKILL.md—pm installand the debuggable-build flag. - Sibling skill:
../../tests/running-instrumented-tests-via-adb/SKILL.md—am instrumentarguments includinguseTestStorageService. - Sibling skill:
../../control/injecting-input-and-state/SKILL.md— driving the scenario that produces the artefact. - Sibling skill:
../../capture/capturing-screenshots-and-screenrecord/SKILL.md— producing PNG / MP4 to pull. - Sibling skill:
../../observability/extracting-logs-with-logcat/SKILL.md— pulling rotated log files viaadb shell logcat -f+adb pull. - Sibling skill:
../../automation/scripting-adb-for-ci/SKILL.md— full CI orchestration. - Cross-set:
../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md—TestWatcherrules wired toTestStorage. - Cross-set:
../../../instrumentation/scenarios/launching-activities-with-activityscenario/SKILL.md— Activity-driven scenarios that emit artefacts. - Cross-set:
../../../fundamentals/strategies/applying-testing-strategies/SKILL.md— when artefacts are the right observability layer.