Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 82 additions & 8 deletions .github/scripts/connected-android-test.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
#!/bin/bash
set -euo pipefail

OUTPUT_ROOT="build/ci-connected-android-test-reports"
echo "reports-path=$OUTPUT_ROOT" >>"$GITHUB_OUTPUT"

# Keep in sync with the "Build Application" step in
# .github/workflows/connected-android-test.yml
TEST_TASKS=(
"app:connectedUnstableDebugAndroidTest"
"core:connectedDebugAndroidTest"
"shared:connectedAndroidDeviceTest"
)

# Environment-instability signatures seen on older API images (e.g. API 24).
# When a failure matches one of these we retry ONCE after restarting adb.
# Anything else is treated as a real failure and must not be retried.
RETRY_SIGNATURES=(
"ShellCommandUnresponsiveException"
"InstallException"
"install-write"
"device offline"
"device not found"
# This might happen when device hang up or activity launch failed
"No compose hierarchies found"
)

TEST_FAILED=0

set_orientation() {
Expand All @@ -12,28 +34,80 @@ set_orientation() {
adb shell settings put system user_rotation "$rotation"
}

is_environment_failure() {
local log=$1 signature
for signature in "${RETRY_SIGNATURES[@]}"; do
if grep -q "$signature" "$log"; then
return 0
fi
done
return 1
}

dump_failure_diagnostics() {
local out="$OUTPUT_ROOT/diagnostics"
mkdir -p "$out"
echo "=== Dumping failure diagnostics to $out ==="
# The device may be half-dead at this point, so tolerate dump failures.
timeout 30 adb logcat -d >"$out/logcat.log" 2>&1 || echo "⚠️ logcat dump failed or timed out"
adb devices -l >"$out/devices.txt" 2>&1 || true
}

run_stage() {
local stage=$1 orientation=$2 attempt
local log
mkdir -p "$OUTPUT_ROOT"
set_orientation "$orientation"

for attempt in 1 2; do
log="$OUTPUT_ROOT/gradle-$stage-attempt$attempt.log"
if ./gradlew "${TEST_TASKS[@]}" --stacktrace 2>&1 | tee "$log"; then
return 0
fi
if [ "$attempt" -eq 1 ] && is_environment_failure "$log"; then
echo "⚠️ Environment failure detected, retrying once"
dump_failure_diagnostics
adb kill-server || true
adb start-server || true
adb wait-for-device || true
else
return 1
fi
done
return 1
}

archive_reports() {
local stage=$1
local target_dir="$OUTPUT_ROOT/$stage"

echo "=== Archiving $stage test reports ==="
mkdir -p "$target_dir/app" "$target_dir/shared"

cp -r app/build/reports/androidTests/connected/* "$target_dir/app/" 2>/dev/null || true
cp -r shared/build/reports/androidTests/connected/* "$target_dir/shared/" 2>/dev/null || true
# Every module listed in TEST_TASKS is expected to produce a connected
# test report dir. A missing/empty one means the gradle run claimed
# success without discoverable results - fail loudly instead of
# silently uploading an empty artifact.
local module src
for module in app core shared; do
src="$module/build/reports/androidTests/connected"
if [ ! -d "$src" ] || [ -z "$(ls -A "$src")" ]; then
echo "❌ No connected test reports found for :$module: (expected at $src)"
exit 1
fi
mkdir -p "$target_dir/$module"
cp -r "$src"/* "$target_dir/$module/"
done
}

echo "=== Starting Portrait Tests ==="
set_orientation 0
if ! ./gradlew connectedDebugAndroidTest --stacktrace; then
if ! run_stage portrait 0; then
echo "❌ Portrait tests failed!"
TEST_FAILED=1
fi
archive_reports "portrait"

echo "=== Starting Landscape Tests ==="
set_orientation 1
if ! ./gradlew connectedDebugAndroidTest --stacktrace; then
if ! run_stage landscape 1; then
echo "❌ Landscape tests failed!"
TEST_FAILED=1
fi
Expand All @@ -44,4 +118,4 @@ if [ $TEST_FAILED -ne 0 ]; then
exit 1
fi

echo "✅ All UI tests passed."
echo "✅ All connected tests passed."
4 changes: 3 additions & 1 deletion .github/workflows/connected-android-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ jobs:
uses: ./.github/actions/setup-ocr-model

# Avoid building the application when AVD is running, so Gradle can use more RAM
# NOTE: keep this task list in sync with TEST_TASKS in .github/scripts/connected-android-test.sh
- name: Build Application
run: ./gradlew assembleDebugAndroidTest --stacktrace
run: ./gradlew app:assembleUnstableDebugAndroidTest core:assembleDebugAndroidTest shared:assembleAndroidDeviceTest --stacktrace

# android-emulator-runner configurations start here
# See also https://github.com/ReactiveCircus/android-emulator-runner#usage--examples
Expand All @@ -73,3 +74,4 @@ jobs:
with:
name: test-results-api${{ matrix.api-level }}-${{ matrix.target }}
path: ${{ steps.ui_tests.outputs.reports-path }}
if-no-files-found: error