Skip to content
Draft
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
99 changes: 99 additions & 0 deletions .github/scripts/run-e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# .github/scripts/run-e2e.sh
#
# Runs the In-App Message E2E test under ReactiveCircus/android-emulator-runner
# and captures diagnostics that survive the action's emulator-kill on exit.
#
# Why this is an external script and not inline YAML:
# The action runs each line of `script:` in a fresh `/bin/sh -c`, so cross-line
# variables and shell functions don't survive. We need a single bash process for
# the trap + variable + function semantics.
#
# Inputs (env, all set by the workflow step):
# ITERABLE_API_KEY — set as buildConfigField at runtime; not echoed.
# ITERABLE_SERVER_API_KEY — set as buildConfigField at runtime; not echoed.
# ITERABLE_TEST_USER_EMAIL — used by tests; not echoed (length only).
# GITHUB_WORKSPACE — set by the runner; root for diagnostics output.
#
# Outputs:
# $GITHUB_WORKSPACE/integration-tests/build/diagnostics/
# hierarchy.xml — UiAutomator dump at the moment of test exit
# screenshot.png — device screenshot at the moment of test exit
# logcat.txt — full device logcat from start of test invocation
#
# Exit code:
# The gradle test task's exit code, propagated.
#
# This script writes nothing outside $GITHUB_WORKSPACE/integration-tests/build/.

set -uo pipefail

readonly TEST_CLASS="${TEST_CLASS:-com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP}"
readonly DIAG_DIR="${GITHUB_WORKSPACE:?GITHUB_WORKSPACE must be set}/integration-tests/build/diagnostics"
readonly TEST_PACKAGE="com.iterable.integration.tests"

mkdir -p "$DIAG_DIR"

log() { printf '\033[1;34m[e2e]\033[0m %s\n' "$*"; }

log "Running E2E test: $TEST_CLASS"
log "Diagnostics will be written to: $DIAG_DIR"

# Sanity-check env: don't echo secret values, only their lengths. The workflow's
# env: block guarantees these vars exist; ${#VAR} of an empty string is 0.
log "ITERABLE_API_KEY length: ${#ITERABLE_API_KEY}"
log "ITERABLE_SERVER_API_KEY length: ${#ITERABLE_SERVER_API_KEY}"
log "ITERABLE_TEST_USER_EMAIL length: ${#ITERABLE_TEST_USER_EMAIL}"

# Grant permissions; ignore failures (the package may not be installed yet,
# in which case AGP will install + auto-grant during the test step).
for perm in POST_NOTIFICATIONS INTERNET ACCESS_NETWORK_STATE WAKE_LOCK; do
adb shell pm grant "$TEST_PACKAGE" "android.permission.$perm" >/dev/null 2>&1 || true
done

# Stream full logcat to the workspace so the artifact upload always has it.
adb logcat -c >/dev/null 2>&1 || true
adb logcat > "$DIAG_DIR/logcat.txt" &
LOGCAT_PID=$!

# Capture diagnostics that depend on a live emulator. Called from EXIT trap so
# we always run, whether tests passed, failed, or the runner timed out.
capture_post_test() {
log "Capturing post-test diagnostics..."

# Stop logcat first so the file isn't being appended to mid-copy.
if [[ -n "${LOGCAT_PID:-}" ]]; then
kill "$LOGCAT_PID" 2>/dev/null || true
wait "$LOGCAT_PID" 2>/dev/null || true
fi

# UiAutomator hierarchy — answers "what was UiAutomator looking at?"
if adb shell uiautomator dump /sdcard/hierarchy.xml >/dev/null 2>&1; then
adb pull /sdcard/hierarchy.xml "$DIAG_DIR/hierarchy.xml" >/dev/null 2>&1 || true
adb shell rm -f /sdcard/hierarchy.xml >/dev/null 2>&1 || true
fi

# Screenshot — answers "what was actually on the screen?"
if adb shell screencap -p /sdcard/screenshot.png >/dev/null 2>&1; then
adb pull /sdcard/screenshot.png "$DIAG_DIR/screenshot.png" >/dev/null 2>&1 || true
adb shell rm -f /sdcard/screenshot.png >/dev/null 2>&1 || true
fi

log "Diagnostics captured:"
ls -la "$DIAG_DIR" || true
}
trap capture_post_test EXIT

# Run the test. Don't `set -e`; we want to capture diagnostics on failure and
# propagate the original exit code at the end.
gradle_exit=0
./gradlew :integration-tests:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class="$TEST_CLASS" \
--stacktrace --no-daemon || gradle_exit=$?

if [[ "$gradle_exit" -ne 0 ]]; then
log "::error::Gradle test task failed with exit code $gradle_exit — see e2e-diagnostics-api artifact"
fi

# capture_post_test runs via EXIT trap; just propagate the exit code.
exit "$gradle_exit"
21 changes: 12 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@216d1ad2b3710bf005dc39237337b9673fd8fcd5 # v3.3.2
uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

- name: Configure JDK
uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2 # v1.4.3
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
java-version: 17
distribution: temurin
java-version: '17'

- run: touch local.properties

Expand All @@ -36,12 +37,13 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@216d1ad2b3710bf005dc39237337b9673fd8fcd5 # v3.3.2
uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

- name: Configure JDK
uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2 # v1.4.3
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
java-version: 17
distribution: temurin
java-version: '17'

- run: touch local.properties

Expand All @@ -66,12 +68,13 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@216d1ad2b3710bf005dc39237337b9673fd8fcd5 # v3.3.2
uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

- name: Configure JDK
uses: actions/setup-java@d202f5dbf7256730fb690ec59f6381650114feb2 # v1.4.3
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
java-version: 17
distribution: temurin
java-version: '17'

- run: touch local.properties

Expand Down
133 changes: 38 additions & 95 deletions .github/workflows/inapp-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,27 @@ on:
jobs:
inapp-e2e-tests:
name: In-App Message E2E Tests
runs-on: macos-15-intel

# SDK-170: macOS Intel runners (2 cores / 3GB AVD on HVF) starved system_server during
# cold boot and produced cascading ANRs (systemui / nexuslauncher / gms / phone …),
# leaving a system dialog on top of MainActivity so UiAutomator could not find the
# in-app button. Ubuntu runners with KVM acceleration and 4 vCPU / 16GB stop the storm.
runs-on: ubuntu-latest

strategy:
matrix:
api-level: [34] # MVP testing on most relevant API level only

steps:
- name: Checkout code
uses: actions/checkout@v4


- name: Enable KVM device permissions
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
Expand Down Expand Up @@ -69,15 +80,15 @@ jobs:
./gradlew :integration-tests:assembleDebug :integration-tests:assembleDebugAndroidTest --no-daemon &
echo "Build started in background..."

- name: Run UI Tests with Emulator (Intel / x86_64)
- name: Run UI Tests with Emulator (KVM / x86_64)
uses: ReactiveCircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: google_apis
arch: x86_64
profile: pixel_6
cores: 2
ram-size: 3072M
cores: 4
ram-size: 4096M
heap-size: 576M
force-avd-creation: true
disable-animations: true
Expand All @@ -87,98 +98,30 @@ jobs:
# Clean + start adb after platform-tools exist (avoids tcp:5037 noise)
adb kill-server >/dev/null 2>&1 || true
adb start-server
script: |
echo "Emulator is ready! Running tests..."
echo "Setting up permissions..."
adb shell pm grant com.iterable.integration.tests android.permission.POST_NOTIFICATIONS
adb shell pm grant com.iterable.integration.tests android.permission.INTERNET
adb shell pm grant com.iterable.integration.tests android.permission.ACCESS_NETWORK_STATE
adb shell pm grant com.iterable.integration.tests android.permission.WAKE_LOCK

echo "Running In-App Message MVP test..."
echo "Debug: Checking if APKs are ready..."
ls -la integration-tests/build/outputs/apk/ || echo "APK directory not found"

echo "Debug: Verifying API keys are set..."
echo "ITERABLE_API_KEY length: ${#ITERABLE_API_KEY}"
echo "ITERABLE_SERVER_API_KEY length: ${#ITERABLE_SERVER_API_KEY}"
echo "ITERABLE_TEST_USER_EMAIL: $ITERABLE_TEST_USER_EMAIL"

# Start logcat in background for crash debugging
adb logcat > /tmp/test-logcat.log &
LOGCAT_PID=$!

# Run the specific test with better error handling
./gradlew :integration-tests:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP \
--stacktrace --no-daemon || {
echo "Test failed! Collecting crash logs..."
kill $LOGCAT_PID 2>/dev/null || true
echo "=== CRASH LOGS ==="
tail -100 /tmp/test-logcat.log
echo "=== END CRASH LOGS ==="
exit 1
}

# Stop logcat
kill $LOGCAT_PID 2>/dev/null || true
# The android-emulator-runner action runs each line of an inline `script:`
# in a fresh `/bin/sh -c`, so cross-line variables and bash functions don't
# survive. Externalise the whole thing to a single bash file that runs in
# one process — see .github/scripts/run-e2e.sh for the actual logic.
script: bash "$GITHUB_WORKSPACE/.github/scripts/run-e2e.sh"
env:
ITERABLE_API_KEY: ${{ secrets.BCIT_ITERABLE_API_KEY }}
ITERABLE_SERVER_API_KEY: ${{ secrets.BCIT_ITERABLE_SERVER_API_KEY }}
ITERABLE_TEST_USER_EMAIL: ${{ secrets.BCIT_ITERABLE_TEST_USER_EMAIL }}

# - name: Generate Test Report
# if: always()
# run: |
# echo "Generating E2E test report..."
# ./gradlew :integration-tests:jacocoIntegrationTestReport

# - name: Collect Test Logs
# if: always()
# run: |
# echo "Collecting E2E test logs..."
# adb logcat -d > integration-tests/build/e2e-test-logs.txt

# # Also collect specific test logs
# adb logcat -d | grep -E "(InAppMessageIntegrationTest|BaseIntegrationTest|IterableApi)" > integration-tests/build/inapp-specific-logs.txt

# - name: Take Screenshots for Debugging
# if: always()
# run: |
# echo "Taking screenshots for debugging..."
# mkdir -p integration-tests/screenshots
# adb shell screencap -p /sdcard/screenshot.png
# adb pull /sdcard/screenshot.png integration-tests/screenshots/final-state-api-${{ matrix.api-level }}.png

# - name: Upload Test Results
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: inapp-e2e-test-results-api-${{ matrix.api-level }}
# path: |
# integration-tests/build/reports/
# integration-tests/build/outputs/
# integration-tests/build/e2e-test-logs.txt
# integration-tests/build/inapp-specific-logs.txt

# - name: Upload Coverage Report
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: inapp-e2e-coverage-api-${{ matrix.api-level }}
# path: integration-tests/build/reports/jacoco/

# - name: Upload Screenshots
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: inapp-e2e-screenshots-api-${{ matrix.api-level }}
# path: integration-tests/screenshots/

# - name: Cleanup
# if: always()
# run: |
# echo "Test cleanup completed"

# SDK-170: do NOT upload integration-tests/build/outputs/ — that path contains the
# built APKs which embed BuildConfig.ITERABLE_API_KEY and BuildConfig.ITERABLE_SERVER_API_KEY
# as compile-time string constants. On a public repo, anyone who can download the
# artifact could `strings`/`apktool` the APK and recover both keys.
- name: Upload E2E diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-diagnostics-api-${{ matrix.api-level }}
path: |
integration-tests/build/diagnostics/
integration-tests/build/reports/
if-no-files-found: warn
retention-days: 7

# test-summary:
# name: Test Summary
Expand Down
Loading
Loading