mcpField
S
scripting-adb-for-ci

scripting-adb-for-ci

github.com/skydovesクラウド・DevOps33970 回インストール22 週間前に更新
リポジトリ

scripting-adb-for-ci について

Android CIで信頼性の高いADBスクリプトを書くためのスキルです。インストゥルメンテーションテストの状態判定、複数デバイスでの並列実行、再試行、タイムアウト、後処理を扱います。

主な機能

  1. 01インストゥルメンテーション状態コードの解析
  2. 02複数デバイスでの並列実行とテスト分割
  3. 03通信エラーの再試行とタイムアウト設定
  4. 04ポート転送の管理と解除
  5. 05失敗時の診断データ収集
  6. 06Test Orchestratorの設定

活用シーン

  1. 01CIでAndroidテストの結果を正確に判定
  2. 02複数デバイスでテストを並列実行
  3. 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 行にあり、そもそも stdout を得るには -w が必要です。
  • チームが単一のエージェントに複数の物理デバイスを接続しており、シャーディングしたテストを並列実行したいと考えています。
  • adb forward と adb reverse が混同されており、ほとんどのチームで引数の順序を逆にしたバグが少なくとも 1 つ発生しています。
  • 一時的な 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 / インストルメンテーションテストの仕組みについて扱う場合。../../../compose/synchronization/synchronizing-with-idle/SKILL.md などを使用してください。

前提条件

  • Bash(この内容のほとんどは zsh でも動作します。timeout を使う場合、macOS ユーザーは gtimeout 用に brew install coreutils を実行してください)。
  • ホストに Platform Tools 24 以降がインストールされていること。古いツールチェーンでは adb shell の終了コードが信頼できません。
  • 安定したシェル終了コードの伝播には API 24 以降のデバイスが必要です。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 を指定すると、runner の stdout が揃うまでシェルが待機します。-w がないと、シェルは使用可能な出力を返さずにすぐ終了します。どちらの場合も、CI の判定には $? ではなく、解析した INSTRUMENTATION_STATUS_CODE 行(下記)を使用してください。

adb shell の終了コード伝播は -x で無効にできます:

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> を出力します。

コードシンボル名意味
1REPORT_VALUE_RESULT_STARTテスト開始
0REPORT_VALUE_RESULT_OKテスト成功
-1REPORT_VALUE_RESULT_ERROR予期しないエラー(プロセス終了、ランナーの失敗)
-2REPORT_VALUE_RESULT_FAILUREアサーション失敗
-3REPORT_VALUE_RESULT_IGNORED(AndroidJUnitRunner の拡張)@Ignore 対象/フィルターで除外
-4REPORT_VALUE_RESULT_ASSUMPTION_FAILURE(AndroidJUnitRunner の拡張)JUnit の assumeXxx が false を返した

-3 や -4 を失敗として扱わないでください。これらはスキップを示します。失敗は -1 と -2 のみです。末尾の INSTRUMENTATION_CODE 行はこれとは無関係で、Activity.RESULT_* の値です(-1 = RESULT_OK = ランナーレベルのクラッシュなし、0 = RESULT_CANCELED = ランナーが終了)。また、シェルの $?(常に 0)と同様に、テストの成功/失敗を示すものではありません。テストごとの 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 ランナー、開発用ノート PC)に接続する必要がある場合。adb reverse tcp:8080 tcp:8080(Metro/ローカルバックエンド)

トークンの構文(adb help より):

トークン使用箇所
tcp:<port>どちら側でも使用できます。LOCAL 側で tcp:0 を指定すると、空いているポートが選ばれ、選択されたポートが標準出力に出力されます。
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。同じエージェントで再実行したときに「cannot bind: address already in use」が発生しないよう、追加したものは必ず trap で削除してください。

タイムアウト — adb -t ではなく timeout でラップする

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 ホストバイナリが 0 以外で終了する場合)に限られることに注意してください。実際のテスト失敗では am instrument は 0 で終了するため、再試行されません。終了コードではなく、出力を解析して検出してください。

複数デバイスへの並列展開

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 は、起動されたシェル内で使う環境変数と同等の指定方法です。

デバイス状態を共有するテスト(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

(詳しくは ../../control/injecting-input-and-state/SKILL.md の pm clear と am force-stop に関する説明を参照してください。)

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 の設定 — androidTestImplementation ではなく androidTestUtil
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'

パッケージ可視性ルールがあっても Orchestrator がターゲットに bindService できるよう、API 30 以降では --force-queryable が必要です。

トレードオフ
  • メリット: 各テストは既知の正常な Application から開始します。1つのクラッシュで残りの実行が停止することはありません。テスト間の 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 を指定し、シェルがランナーの完了を待って、解析可能な生の出力を出力するようにしてください。-w を指定しないと、出力が生成される前にシェルが戻ります。
  • am instrument の $? で CI の合否を判定してはいけません。テスト結果にかかわらず(-w の有無にかかわらず)、System.exit(0) が呼び出されます。stdout から INSTRUMENTATION_STATUS_CODE: -1(エラー)と -2(失敗)を検索して失敗を検出してください。
  • INSTRUMENTATION_STATUS_CODE を解析するとき、-3 の IGNORED と -4 の ASSUMPTION_FAILURE を実際の失敗と区別する必要があります。失敗コードは -1 と -2 です。
  • adb forward は LOCAL REMOTE(ホストが先)、adb reverse は REMOTE LOCAL(デバイスが先)であることを必ず覚えておいてください。引数の順序は逆です。
  • 実行時間の長い adb コマンドは、必ず timeout <N>s(macOS では gtimeout)でラップしてください。adb に組み込みのタイムアウトフラグはありません。adb -t はトランスポート ID であり、タイムアウトではありません。
  • 必ず androidTestImplementation ではなく androidTestUtil("androidx.test:orchestrator:1.6.1") を使用してください。オーケストレーターはライブラリではなく、独立した APK です。
  • 一時的な adb エラー(device not found、closed、protocol fault)が発生した場合は、試行の間に adb kill-server; adb start-server を実行する、上限付きの retry ヘルパーで必ず再試行してください。
  • trap cleanup EXIT を必ず設定し、ポートフォワードを削除(adb forward --remove-all、adb reverse --remove-all)し、アニメーションスケールを復元して、テスト対象アプリに対して pm clear を実行してください。クリーンアップがテストの終了コードを上書きしないよう、各手順に || true を付けてください。
  • 各テスト実行の前に(am force-stop だけでなく)必ず pm clear <pkg> を実行してください。../../control/injecting-input-and-state/SKILL.md を参照してください。
  • API 24 未満のデバイスでは、adb shell の終了コード伝播に依存してはいけません。古いターゲットでは stdout にセンチネル __ADB_RC=$? を出力するか、テストインフラの minSdk を固定してください。
  • 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 が指定されており、合否の判定に $? を使うスクリプト分岐がないこと。失敗は stdout から INSTRUMENTATION_STATUS_CODE: -1/-2 を解析して検出します。
  • adb forward と adb reverse の引数が記載された順序で指定されていること。CI で、ホストからデバイスのサービスに到達でき、その逆も可能であることを確認するスモークテストに合格すること。
  • androidTestUtil('androidx.test:orchestrator:...') を使用している(androidTestImplementation ではない)こと。./gradlew :app:dependencies --configuration androidTestUtil にオーケストレーターが表示されること。
  • トップレベルの 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 — ハーメチックな前処理(アニメーションを 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 — ランナーの内部動作。
  • セット横断:../../../instrumentation/scenarios/launching-activities-with-activityscenario/SKILL.md — インストルメンテーション中の Activity シナリオ。
  • セット横断:../../../fundamentals/strategies/applying-testing-strategies/SKILL.md — CI オーケストレーションに投資する場合と、よりシンプルなツールを使う場合の判断。

コメント