mcpField
E
extracting-test-artifacts

extracting-test-artifacts

github.com/skydovesTesting & quality33964 installsUpdated 22w ago
Repository

About extracting-test-artifacts

A practical Android testing skill for developers who need to move test files between a host and a device. It covers adb permissions, binary-safe extraction with run-as, and AndroidX TestStorage for collecting per-test artifacts.

Key features

  1. 01Explains adb push and pull usage
  2. 02Maps device paths to access permissions
  3. 03Uses run-as for app-private files
  4. 04Shows binary-safe extraction with exec-out
  5. 05Covers AndroidX TestStorage artifact collection

Use cases

  1. 01Retrieving a database from a debuggable test app
  2. 02Saving screenshots or JSON files as CI artifacts
  3. 03Moving large test fixtures to a device

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/skydoves/android-testing-skills --skill extracting-test-artifacts

After installing, type/extracting-test-artifactsin Claude Code to confirm it loaded.

Details

From the project README

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.db fails with "Permission denied"; the developer doesn't know run-as exists.
  • A binary file (PNG, MP4, SQLite DB) arrives on the host with mangled bytes — the missing exec-out is 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 TestStorage API.

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-state returns device. See ../../devices/connecting-to-devices/SKILL.md.
  • For run-as: the target APK must be android:debuggable="true" in its merged manifest (the default debug build variant).
  • For -z brotli/zstd: platform-tools 30+ on the host and adbd ≥ Android 11 on the device.
  • For TestStorage: androidx.test:services and the -e useTestStorageService true argument to am instrument.

Symmetric syntax

"Unlike the install command, which only copies an APK file to a specific location, the pull and push commands 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
FlagMeaning
-z <algo>Enable compression. <algo> ∈ any / none / brotli / lz4 / zstd. any lets adbd pick.
-ZDisable compression.
--sync (push only)Only push files with different timestamps — fast incremental updates.
-a (pull only)Preserve file timestamp and mode.
-nDry-run.
-qSuppress 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.

Pathadb shell writable?pull works?
/data/local/tmp/YesYes
/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 storageYes 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 root on userdebug/eng builds (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
  1. Work on release / non-debuggable builds. Will print "Package is not debuggable".
  2. Switch to a system UID (only the target package's UID).
  3. Cross profile boundaries by itself. Use --user N if needed.

Common test-artefact paths

PathOrigin
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-*.xmlJUnit-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 connectedAndroidTest automatically pulls them into app/build/outputs/connected_android_test_additional_output/.
  • Per-test artefact namespacing happens through the runner, not via hand-rolled Class.method filename 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> (not adb 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 no adb pull equivalent that works through run-as.
  • MUST verify the build is debuggable before scripting run-as. On a release APK, run-as exits non-zero with "Package is not debuggable".
  • MUST prefer /sdcard/Android/data/<pkg>/files/ (via Context.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's connected_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 -q to silence).
  • MUST NOT assume adb pull of /data/data/<pkg>/... works without run-as. It does not, even on debuggable builds — pull is a shell-user operation.
  • PREFERRED: --sync -z brotli for 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 through run-as.
  • Binary-file pulls use adb exec-out, not adb 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 --sync so unchanged files are skipped.
  • file reports the correct binary type for every pulled artefact (no CRLF corruption).
  • Release APKs have no test code paths that try to adb pull from /data/data/<pkg>/... — those would fail.

References

  • developer.android.com/tools/adb — pull/push syntax, "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 at module/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 install and the debuggable-build flag.
  • Sibling skill: ../../tests/running-instrumented-tests-via-adb/SKILL.md — am instrument arguments including useTestStorageService.
  • 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 via adb 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 — TestWatcher rules wired to TestStorage.
  • 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.

Comments

Markdown supported