From a214be4b11489518c01afad73b0d61598bb51ab8 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Tue, 28 Apr 2026 10:37:34 +0100 Subject: [PATCH 1/8] Fixing BCIT and in app tests in CI --- integration-tests/build.gradle | 30 + .../tests/InAppMessageIntegrationTest.kt | 14 + scripts/trace-bcit.sh | 526 ++++++++++++++++++ 3 files changed, 570 insertions(+) create mode 100755 scripts/trace-bcit.sh diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 4ef5616a2..edb8f04d8 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -119,6 +119,36 @@ dependencies { androidTestImplementation 'com.google.code.gson:gson:2.8.9' } +// SDK-170 β€” debug task that surfaces the build-time BuildConfig inputs the test APK was +// built with. We only print *length* and *first 4 chars* of secrets so they never appear +// in CI logs verbatim. Used by scripts/trace-bcit.sh. +tasks.register('printBuildConfig') { + group = 'verification' + description = 'Print the integration-test BuildConfig inputs as seen by the build (length-only for secrets).' + doLast { + def localProperties = new Properties() + def localPropertiesFile = rootProject.file('local.properties') + if (localPropertiesFile.exists()) { + localPropertiesFile.withInputStream { localProperties.load(it) } + } + def showPrefix = System.getenv('TRACE_SHOW_PREFIX') == '1' + def report = { String name -> + def fromLocal = localProperties.getProperty(name) + def fromEnv = System.getenv(name) + def chosen = fromLocal ?: fromEnv ?: '' + def source = fromLocal ? 'local.properties' : (fromEnv ? 'env' : 'default-fallback') + def line = "${name} length=${chosen.length()} source=${source}" + if (showPrefix && chosen.length() >= 4) { + line += " prefix=${chosen.substring(0, 4)}\u2026" + } + println line + } + report('ITERABLE_API_KEY') + report('ITERABLE_SERVER_API_KEY') + report('ITERABLE_TEST_USER_EMAIL') + } +} + // Jacoco coverage for integration tests tasks.withType(Test) { jacoco.includeNoLocationClasses = true diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt index ace6b02af..20d3fd7bf 100644 --- a/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt @@ -55,6 +55,20 @@ class InAppMessageIntegrationTest : BaseIntegrationTest() { super.setUp() Log.d(TAG, "πŸ”§ Base setup complete, SDK initialized with test handlers") + + // SDK-170: Pause in-app auto-display and drain the inbox BEFORE launching the + // activity. Otherwise messages already staged for the test user will be fetched + // by setEmail() during super.setUp() and shown over MainActivity, occluding the + // buttons UiAutomator is about to look for. The test calls + // inAppManager.showMessage() explicitly later, which still works while paused. + // Pattern matches PushNotificationIntegrationTest / EmbeddedMessageIntegrationTest / + // DeepLinkIntegrationTest. + IterableApi.getInstance().inAppManager.setAutoDisplayPaused(true) + IterableApi.getInstance().inAppManager.messages.forEach { + Log.d(TAG, "Clearing pre-existing in-app message before navigation: ${it.messageId}") + IterableApi.getInstance().inAppManager.removeMessage(it) + } + Log.d(TAG, "πŸ”§ MainActivity will skip initialization due to test mode flag") // Now launch the app flow with custom handlers already configured diff --git a/scripts/trace-bcit.sh b/scripts/trace-bcit.sh new file mode 100755 index 000000000..c7f185256 --- /dev/null +++ b/scripts/trace-bcit.sh @@ -0,0 +1,526 @@ +#!/usr/bin/env bash +# trace-bcit.sh β€” End-to-end path tracing harness for SDK-170. +# +# Captures 10 checkpoints along the path: +# β‘  emulator subprocess +# β‘‘ hostβ†’guest network bring-up +# β‘’ guest network state (ConnectivityService, netd, wifi) +# β‘£ DNS reachability of api.iterable.com +# β‘€ TCP/TLS reachability of api.iterable.com:443 +# β‘₯ build-time inputs (BuildConfig.ITERABLE_API_KEY length only β€” never the value) +# ⑦ SDK init order +# β‘§ SDK HTTP request lifecycle (logcat) +# ⑨ test outcome (junit reports) +# β‘© device screenshot at end +# +# Designed to run both locally (macOS) and inside a GitHub Actions step +# wrapped by ReactiveCircus/android-emulator-runner. When the env var +# TRACE_MODE=ci-script is set, the script assumes the emulator is already +# running (the action launched it) and skips its own emulator lifecycle. +# +# All output is written to a single timestamped folder under TRACE_OUT_ROOT +# (default: /tmp). Nothing is written outside the repo or that folder. +# +# Exit codes: +# 0 β€” full trace completed (test may have passed or failed; see 08-summary.md) +# 2 β€” fatal setup error (e.g. emulator wouldn't boot, can't write artifacts) + +set -uo pipefail + +# ------------------------------------------------------------------------------ +# Config +# ------------------------------------------------------------------------------ + +readonly REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly TRACE_MODE="${TRACE_MODE:-local}" # local | ci-script +readonly API_LEVEL="${API_LEVEL:-34}" +readonly TARGET="${TARGET:-google_apis}" +readonly ARCH="${ARCH:-arm64-v8a}" # arm64-v8a locally, x86_64 in CI +readonly AVD_NAME="${AVD_NAME:-bcit_trace_avd}" +readonly EMULATOR_PORT="${EMULATOR_PORT:-5554}" +readonly EMULATOR_SERIAL="emulator-${EMULATOR_PORT}" +readonly TEST_CLASS="com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP" +readonly TRACE_OUT_ROOT="${TRACE_OUT_ROOT:-/tmp}" +readonly TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +readonly TRACE_DIR="${TRACE_OUT_ROOT}/sdk-170-trace-${TIMESTAMP}" + +# Probe targets +readonly TARGET_HOST="api.iterable.com" +readonly TARGET_PORT="443" + +# ------------------------------------------------------------------------------ +# Logging +# ------------------------------------------------------------------------------ + +log() { printf '\033[1;34m[trace]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*" >&2; } +err() { printf '\033[1;31m[err]\033[0m %s\n' "$*" >&2; } +die() { err "$*"; exit 2; } + +run_step() { + # run_step -- + # Records the command, captures stdout+stderr, never aborts the harness. + local name="$1"; shift + local out="$1"; shift + [[ "$1" == "--" ]] && shift + printf '# %s\n# cmd: %s\n# t: %s\n\n' "$name" "$*" "$(date -u +%FT%TZ)" > "$out" + if "$@" >> "$out" 2>&1; then + log " βœ“ ${name}" + else + warn " βœ— ${name} (exit $?). See ${out#"${TRACE_DIR}/"}" + fi +} + +adb_shell() { + "$ADB" -s "${EMULATOR_SERIAL}" shell "$@" +} + +# ------------------------------------------------------------------------------ +# Tool resolution +# ------------------------------------------------------------------------------ + +find_tools() { + local sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-${HOME}/Library/Android/sdk}}" + [[ -d "$sdk" ]] || die "ANDROID_HOME not set and ${sdk} doesn't exist" + + EMULATOR="${sdk}/emulator/emulator" + ADB="${sdk}/platform-tools/adb" + AVDMANAGER="${sdk}/cmdline-tools/latest/bin/avdmanager" + SDKMANAGER="${sdk}/cmdline-tools/latest/bin/sdkmanager" + + for tool in "$EMULATOR" "$ADB"; do + [[ -x "$tool" ]] || die "missing tool: $tool" + done + if [[ "$TRACE_MODE" == "local" ]]; then + [[ -x "$AVDMANAGER" ]] || warn "avdmanager not at $AVDMANAGER (local mode needs it)" + [[ -x "$SDKMANAGER" ]] || warn "sdkmanager not at $SDKMANAGER (local mode needs it)" + fi + + log "ANDROID_HOME = $sdk" + log "emulator = $("$EMULATOR" -version 2>&1 | head -1)" + log "adb = $("$ADB" version | head -1)" +} + +# ------------------------------------------------------------------------------ +# Step 0 β€” environment snapshot +# ------------------------------------------------------------------------------ + +step_00_env() { + log "Step 0 β€” environment snapshot" + local out="${TRACE_DIR}/00-env.txt" + { + echo "# Trace mode: $TRACE_MODE" + echo "# Trace dir: $TRACE_DIR" + echo "# Timestamp: $TIMESTAMP" + echo + echo "## Host" + echo "uname -a: $(uname -a)" + if command -v sw_vers >/dev/null 2>&1; then + echo "sw_vers: $(sw_vers -productName) $(sw_vers -productVersion)" + fi + echo "arch: $(uname -m)" + echo "user: ${USER:-?}" + echo "shell: ${SHELL:-?}" + echo + echo "## Android SDK" + echo "ANDROID_HOME=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-${HOME}/Library/Android/sdk}}" + "$EMULATOR" -version 2>&1 | head -2 + "$ADB" version | head -2 + echo + echo "## Trace target" + echo "API_LEVEL=$API_LEVEL TARGET=$TARGET ARCH=$ARCH" + echo "AVD=$AVD_NAME PORT=$EMULATOR_PORT" + echo "TEST_CLASS=$TEST_CLASS" + echo + echo "## CI variables (presence only)" + for v in GITHUB_ACTIONS GITHUB_RUN_ID GITHUB_SHA RUNNER_OS RUNNER_ARCH; do + eval "val=\${$v:-}" + printf '%s=%s\n' "$v" "${val:+SET}" + done + echo + echo "## Iterable secrets (presence + length only)" + for v in ITERABLE_API_KEY ITERABLE_SERVER_API_KEY ITERABLE_TEST_USER_EMAIL; do + eval "val=\${$v:-}" + printf '%-32s present=%s length=%s\n' "$v" "$([[ -n "$val" ]] && echo yes || echo no)" "${#val}" + done + } > "$out" +} + +# ------------------------------------------------------------------------------ +# Step 1 β€” emulator launch (local only; in CI the action handles it) +# ------------------------------------------------------------------------------ + +ensure_system_image() { + local pkg="system-images;android-${API_LEVEL};${TARGET};${ARCH}" + if "$SDKMANAGER" --list_installed 2>/dev/null | grep -q "$pkg"; then + log "system image already installed: $pkg" + return 0 + fi + log "installing system image: $pkg (this may take a few minutes)" + yes | "$SDKMANAGER" --licenses >/dev/null 2>&1 || true + "$SDKMANAGER" "$pkg" 2>&1 | tail -5 || die "failed to install $pkg" +} + +ensure_avd() { + if "$EMULATOR" -list-avds 2>/dev/null | grep -qx "$AVD_NAME"; then + log "AVD already exists: $AVD_NAME (deleting for clean baseline)" + "$AVDMANAGER" delete avd -n "$AVD_NAME" >/dev/null 2>&1 || true + fi + local pkg="system-images;android-${API_LEVEL};${TARGET};${ARCH}" + log "creating AVD: $AVD_NAME ($pkg, profile=pixel_6)" + echo "no" | "$AVDMANAGER" create avd \ + -n "$AVD_NAME" \ + -k "$pkg" \ + -d "pixel_6" \ + --force >/dev/null + # Match CI sizing: ram-size 3072M, heap-size 576M (set in config.ini). + local cfg="${HOME}/.android/avd/${AVD_NAME}.avd/config.ini" + if [[ -f "$cfg" ]]; then + { + echo "hw.ramSize=3072" + echo "vm.heapSize=576" + echo "disk.dataPartition.size=6000M" + echo "hw.keyboard=yes" + } >> "$cfg" + fi +} + +step_01_launch_emulator() { + if [[ "$TRACE_MODE" != "local" ]]; then + log "Step 1 β€” emulator already running (CI mode)" + return 0 + fi + log "Step 1 β€” launching emulator" + ensure_system_image + ensure_avd + local emu_log="${TRACE_DIR}/01-emulator.log" + # CI flags: -no-window -no-snapshot -gpu swiftshader_indirect -no-boot-anim -camera-back none -partition-size 6000 + nohup "$EMULATOR" \ + -avd "$AVD_NAME" \ + -port "$EMULATOR_PORT" \ + -no-window \ + -no-snapshot \ + -no-boot-anim \ + -gpu swiftshader_indirect \ + -camera-back none \ + -partition-size 6000 \ + -no-audio \ + >"$emu_log" 2>&1 & + EMU_PID=$! + echo "$EMU_PID" > "${TRACE_DIR}/.emulator.pid" + log " emulator PID=$EMU_PID, port=$EMULATOR_PORT, log=01-emulator.log" + trap 'cleanup' EXIT +} + +cleanup() { + if [[ -f "${TRACE_DIR}/.emulator.pid" ]]; then + local pid + pid="$(cat "${TRACE_DIR}/.emulator.pid")" + log "stopping emulator PID=$pid" + "$ADB" -s "${EMULATOR_SERIAL}" emu kill 2>/dev/null || true + sleep 2 + kill "$pid" 2>/dev/null || true + fi +} + +# ------------------------------------------------------------------------------ +# Step 2 β€” boot +# ------------------------------------------------------------------------------ + +step_02_boot() { + log "Step 2 β€” wait for boot" + mkdir -p "${TRACE_DIR}/02-boot" + local boot_log="${TRACE_DIR}/02-boot/poll.txt" + : > "$boot_log" + + log " waiting for adb to detect device (timeout 90s)..." + local t=0 + while ! "$ADB" devices | grep -q "${EMULATOR_SERIAL}.*device"; do + sleep 2; t=$((t+2)) + [[ $t -ge 90 ]] && { warn " device not detected after 90s"; "$ADB" devices >> "$boot_log"; return 1; } + done + + log " device detected, polling sys.boot_completed (timeout 300s)..." + t=0 + while :; do + local boot trace + boot="$(adb_shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" + trace="$(adb_shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" + printf 't=%ds sys.boot_completed=%q dev.bootcomplete=%q\n' "$t" "$boot" "$trace" >> "$boot_log" + [[ "$boot" == "1" ]] && break + sleep 3; t=$((t+3)) + if [[ $t -ge 300 ]]; then + warn " boot timeout after 300s" + adb_shell logcat -d -t 200 > "${TRACE_DIR}/02-boot/logcat-during-boot.txt" 2>&1 || true + return 1 + fi + done + + log " booted at t=${t}s" + echo "boot_seconds=${t}" >> "$boot_log" + # Match CI: disable animations + for k in window_animation_scale transition_animation_scale animator_duration_scale; do + adb_shell settings put global "$k" 0 || true + done + + # SDK-170: sys.boot_completed=1 is NOT a sufficient signal β€” Wi-Fi association + # via the virtio-wifi/netsim stack lags boot by 20-90s. Poll for an active + # default network and reachable internet before we declare the emulator ready. + log " waiting for default network (timeout 180s)..." + local n=0 + local default_net="none" + while [[ "$default_net" == "none" ]]; do + default_net="$(adb_shell 'dumpsys connectivity | grep -E "^Active default network" | head -1' 2>/dev/null | sed 's/.*: //;s/[[:space:]]*$//' | tr -d '\r')" + printf 't=%ds active_default_network=%q\n' "$n" "$default_net" >> "$boot_log" + [[ "$default_net" != "none" && -n "$default_net" ]] && break + sleep 3; n=$((n+3)) + if [[ $n -ge 180 ]]; then + warn " default network never came up within 180s" + echo "default_network_timeout=true" >> "$boot_log" + break + fi + done + log " default network up at t=${n}s after boot (default=${default_net})" + echo "default_network_seconds=${n}" >> "$boot_log" + + log " waiting for internet reachability (ping 8.8.8.8 succeeds; timeout 60s)..." + local p=0 + while ! adb_shell 'ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1'; do + sleep 2; p=$((p+2)) + if [[ $p -ge 60 ]]; then + warn " internet unreachable after 60s of polling" + echo "internet_timeout=true" >> "$boot_log" + return 0 + fi + done + log " internet reachable at t=${p}s after default network" + echo "internet_seconds=${p}" >> "$boot_log" + return 0 +} + +# ------------------------------------------------------------------------------ +# Step 3 β€” guest network state +# ------------------------------------------------------------------------------ + +step_03_guest_network() { + log "Step 3 β€” guest network state" + mkdir -p "${TRACE_DIR}/03-network-guest" + local d="${TRACE_DIR}/03-network-guest" + + run_step "ifconfig" "$d/ifconfig.txt" -- adb_shell ifconfig + run_step "ip-route" "$d/ip-route.txt" -- adb_shell ip route + run_step "ip-addr" "$d/ip-addr.txt" -- adb_shell ip addr + run_step "getprop-net" "$d/getprop-net.txt" -- adb_shell "getprop | grep -E 'net\\.|wifi\\.'" + run_step "connectivity" "$d/connectivity.txt" -- adb_shell dumpsys connectivity + run_step "wifi" "$d/wifi.txt" -- adb_shell dumpsys wifi + run_step "netd-resolver" "$d/netd.txt" -- adb_shell ndc resolver dump + run_step "init.svc.netd" "$d/init-netd.txt" -- adb_shell getprop init.svc.netd +} + +# ------------------------------------------------------------------------------ +# Step 4 β€” DNS + Step 5 β€” TCP/TLS reachability of api.iterable.com +# ------------------------------------------------------------------------------ + +step_04_dns_tcp() { + log "Step 4+5 β€” DNS / TCP / TLS reachability of ${TARGET_HOST}" + mkdir -p "${TRACE_DIR}/04-dns-tcp" + local d="${TRACE_DIR}/04-dns-tcp" + + run_step "ping-iterable" "$d/ping.txt" -- adb_shell ping -c 5 -W 3 "$TARGET_HOST" + run_step "ping-8.8.8.8" "$d/ping-dns.txt" -- adb_shell ping -c 3 -W 3 8.8.8.8 + run_step "nslookup" "$d/nslookup.txt" -- adb_shell nslookup "$TARGET_HOST" + run_step "getent-hosts" "$d/getent.txt" -- adb_shell getent hosts "$TARGET_HOST" + + # Java-level probe via a tiny inline script that uses the SDK's classloader. + # We skip that here and rely on the test step (07/08) to surface in-app HTTP. + # Instead, run a host-side curl so we know whether the network exit is reachable + # from the runner (CI baseline). + run_step "host-curl-iterable" "$d/host-curl.txt" -- bash -c \ + "curl -sS -o /dev/null -w 'http=%{http_code}\\ntcp=%{time_connect}\\ntls=%{time_appconnect}\\ntotal=%{time_total}\\nremoteip=%{remote_ip}\\n' --max-time 10 'https://${TARGET_HOST}/api/inApp/getMessages?email=trace@iterable.com'" +} + +# ------------------------------------------------------------------------------ +# Step 6 β€” build with diagnostics +# ------------------------------------------------------------------------------ + +step_06_build() { + log "Step 6 β€” build APK + capture BuildConfig (key length only)" + mkdir -p "${TRACE_DIR}/05-apk" + local d="${TRACE_DIR}/05-apk" + + pushd "$REPO_ROOT" >/dev/null + run_step "gradle-version" "$d/gradle-version.txt" -- ./gradlew --version + run_step "gradle-printBuildConfig" "$d/printBuildConfig.txt" -- \ + ./gradlew :integration-tests:printBuildConfig -q + run_step "gradle-assemble" "$d/gradle-assemble.txt" -- \ + ./gradlew :integration-tests:assembleDebug :integration-tests:assembleDebugAndroidTest --no-daemon + popd >/dev/null + + # APK introspection + local apk + apk="$(ls -t "$REPO_ROOT"/integration-tests/build/outputs/apk/debug/*.apk 2>/dev/null | head -1 || true)" + if [[ -n "$apk" ]]; then + cp "$apk" "$d/installed.apk" 2>/dev/null || true + if command -v aapt2 >/dev/null 2>&1; then + run_step "aapt2-dump" "$d/aapt2-dump.txt" -- aapt2 dump badging "$apk" + elif command -v aapt >/dev/null 2>&1; then + run_step "aapt-dump" "$d/aapt-dump.txt" -- aapt dump badging "$apk" + fi + else + warn " APK not found after build" + fi +} + +# ------------------------------------------------------------------------------ +# Step 7+8+9+10 β€” install, run test, capture +# ------------------------------------------------------------------------------ + +step_07_install_run() { + log "Step 7-9 β€” install + run instrumented test" + mkdir -p "${TRACE_DIR}/06-test-run/junit-reports" + mkdir -p "${TRACE_DIR}/06-test-run/screenshots" + mkdir -p "${TRACE_DIR}/07-logcat" + local d="${TRACE_DIR}/06-test-run" + + # Start logcat in background + local logcat_pid_file="${TRACE_DIR}/.logcat.pid" + "$ADB" -s "${EMULATOR_SERIAL}" logcat -c >/dev/null 2>&1 || true + "$ADB" -s "${EMULATOR_SERIAL}" logcat > "${TRACE_DIR}/07-logcat/full.txt" 2>&1 & + echo $! > "$logcat_pid_file" + + # Pre-test screenshot + adb_shell screencap -p /sdcard/pre-test.png >/dev/null 2>&1 || true + "$ADB" -s "${EMULATOR_SERIAL}" pull /sdcard/pre-test.png "$d/screenshots/pre-test.png" >/dev/null 2>&1 || true + + pushd "$REPO_ROOT" >/dev/null + log " running test: ${TEST_CLASS}" + ./gradlew :integration-tests:connectedDebugAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.class="${TEST_CLASS}" \ + --stacktrace --no-daemon \ + > "$d/gradle.log" 2>&1 + local rc=$? + echo "$rc" > "$d/gradle.exit-code" + log " test gradle exit code: $rc" + popd >/dev/null + + # Post-test screenshot + adb_shell screencap -p /sdcard/post-test.png >/dev/null 2>&1 || true + "$ADB" -s "${EMULATOR_SERIAL}" pull /sdcard/post-test.png "$d/screenshots/post-test.png" >/dev/null 2>&1 || true + + # Grab the UiAutomator hierarchy dump if the test left one behind (SDK-170 diag) + "$ADB" -s "${EMULATOR_SERIAL}" pull /sdcard/Download/uiautomator-after-launch.xml \ + "$d/uiautomator-after-launch.xml" >/dev/null 2>&1 || true + + # Also dump current window hierarchy for post-mortem + adb_shell uiautomator dump /sdcard/post-test-hierarchy.xml >/dev/null 2>&1 || true + "$ADB" -s "${EMULATOR_SERIAL}" pull /sdcard/post-test-hierarchy.xml \ + "$d/post-test-hierarchy.xml" >/dev/null 2>&1 || true + + # Stop logcat + if [[ -f "$logcat_pid_file" ]]; then + kill "$(cat "$logcat_pid_file")" 2>/dev/null || true + fi + + # Copy junit reports + cp -R "$REPO_ROOT/integration-tests/build/reports/androidTests/connected/." \ + "$d/junit-reports/" 2>/dev/null || true + + # Filtered logcats + grep -E 'IterableApi|IterableRequest|IterableInApp|IterableEmbedded|IntegrationMainActivity|BaseIntegrationTest' \ + "${TRACE_DIR}/07-logcat/full.txt" > "${TRACE_DIR}/07-logcat/iterable.txt" 2>/dev/null || true + grep -E 'AndroidRuntime|FATAL|ANR|SIGSEGV|System\.err' \ + "${TRACE_DIR}/07-logcat/full.txt" > "${TRACE_DIR}/07-logcat/crashes.txt" 2>/dev/null || true + + return 0 +} + +# ------------------------------------------------------------------------------ +# Step 11 β€” summary +# ------------------------------------------------------------------------------ + +# helper: print first non-empty grep match or "β€”" +peek() { grep -m1 -E "$1" "$2" 2>/dev/null | sed 's/[[:space:]]\+$//' || echo "β€”"; } + +step_99_summary() { + log "Writing 08-summary.md" + local s="${TRACE_DIR}/08-summary.md" + local d_net="${TRACE_DIR}/03-network-guest" + local d_dns="${TRACE_DIR}/04-dns-tcp" + local d_apk="${TRACE_DIR}/05-apk" + local d_test="${TRACE_DIR}/06-test-run" + local d_log="${TRACE_DIR}/07-logcat" + + local boot_seconds default_net_seconds internet_seconds + boot_seconds="$(grep '^boot_seconds=' "${TRACE_DIR}/02-boot/poll.txt" 2>/dev/null | cut -d= -f2 || echo '?')" + default_net_seconds="$(grep '^default_network_seconds=' "${TRACE_DIR}/02-boot/poll.txt" 2>/dev/null | cut -d= -f2 || echo '?')" + internet_seconds="$(grep '^internet_seconds=' "${TRACE_DIR}/02-boot/poll.txt" 2>/dev/null | cut -d= -f2 || echo '?')" + local key_len + key_len="$(grep -E 'ITERABLE_API_KEY' "$d_apk/printBuildConfig.txt" 2>/dev/null | head -1 || echo 'β€”')" + local ping_loss + ping_loss="$(grep -Eo '[0-9]+% packet loss' "$d_dns/ping.txt" 2>/dev/null | head -1 || echo 'β€”')" + local host_http + host_http="$(grep -E '^http=' "$d_dns/host-curl.txt" 2>/dev/null | head -1 || echo 'β€”')" + local test_rc + test_rc="$(cat "$d_test/gradle.exit-code" 2>/dev/null || echo '?')" + + { + echo "# SDK-170 trace summary" + echo + echo "- timestamp: \`${TIMESTAMP}\`" + echo "- mode: \`${TRACE_MODE}\`" + echo "- arch: \`$(uname -m)\` API \`${API_LEVEL}\` ${TARGET}/${ARCH}" + echo + echo "## Checkpoints" + echo + echo "| # | Checkpoint | Result |" + echo "|---|---------------------------------------------|--------|" + echo "| β‘  | emulator launched | $([[ -s "${TRACE_DIR}/01-emulator.log" || "$TRACE_MODE" == ci-script ]] && echo PASS || echo FAIL) |" + echo "| β‘‘ | boot completed (sys.boot_completed=1) | ${boot_seconds}s |" + echo "| β‘‘ | default network up (after boot) | +${default_net_seconds}s |" + echo "| β‘‘ | internet reachable (ping 8.8.8.8) | +${internet_seconds}s |" + echo "| β‘’ | wlan0 has IP | $(peek '^[[:space:]]*inet ' "$d_net/ifconfig.txt") |" + echo "| β‘’ | default route | $(peek 'default' "$d_net/ip-route.txt") |" + echo "| β‘’ | net.dns1 | $(peek '^net\\.dns1' "$d_net/getprop-net.txt") |" + echo "| β‘£ | DNS api.iterable.com | $(peek 'address|server can' "$d_dns/nslookup.txt") |" + echo "| β‘€ | ping api.iterable.com loss | ${ping_loss} |" + echo "| β‘€ | host curl https://api.iterable.com | ${host_http} |" + echo "| β‘₯ | BuildConfig key | ${key_len} |" + echo "| ⑦ | SDK init in logcat | $([[ -s "$d_log/iterable.txt" ]] && grep -m1 -c 'Iterable SDK initialized' "$d_log/iterable.txt" || echo 0) hits |" + echo "| β‘§ | iterable HTTP req in logcat | $([[ -s "$d_log/iterable.txt" ]] && grep -c -E 'Sending request|response code' "$d_log/iterable.txt" || echo 0) hits |" + echo "| ⑨ | gradle test exit code | ${test_rc} |" + echo "| β‘© | post-test screenshot | $([[ -f "$d_test/screenshots/post-test.png" ]] && echo present || echo missing) |" + echo + echo "## Top crashes (if any)" + echo + echo '```' + head -30 "$d_log/crashes.txt" 2>/dev/null || echo "(none)" + echo '```' + echo + echo "## Files" + echo + find "$TRACE_DIR" -maxdepth 2 -type f | sed "s|${TRACE_DIR}/||" | sort + } > "$s" + log "summary written: $s" +} + +# ------------------------------------------------------------------------------ +# Main +# ------------------------------------------------------------------------------ + +main() { + mkdir -p "$TRACE_DIR" || die "cannot create $TRACE_DIR" + log "Trace output: $TRACE_DIR" + find_tools + step_00_env + step_01_launch_emulator || die "emulator launch failed" + step_02_boot || warn "boot incomplete; continuing for partial trace" + step_03_guest_network + step_04_dns_tcp + step_06_build + step_07_install_run + step_99_summary + log "Done. Trace: $TRACE_DIR" + log " open ${TRACE_DIR}/08-summary.md" +} + +main "$@" From 682683cc4d6d7cb131caa4df93e511fb361d1d8d Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Tue, 28 Apr 2026 12:08:11 +0100 Subject: [PATCH 2/8] observability --- .github/workflows/inapp-e2e-tests.yml | 170 +++++++++++++++----------- 1 file changed, 96 insertions(+), 74 deletions(-) diff --git a/.github/workflows/inapp-e2e-tests.yml b/.github/workflows/inapp-e2e-tests.yml index ba68dbd6c..1a2d66ea1 100644 --- a/.github/workflows/inapp-e2e-tests.yml +++ b/.github/workflows/inapp-e2e-tests.yml @@ -88,97 +88,119 @@ jobs: adb kill-server >/dev/null 2>&1 || true adb start-server script: | + # SDK-170 OBSERVABILITY: capture diagnostics that survive the action exit. + # The android-emulator-runner kills the emulator on its way out, so any + # adb-dependent capture MUST happen before this script returns. We write + # everything under $GITHUB_WORKSPACE so a later step can upload it. + DIAG_DIR="$GITHUB_WORKSPACE/integration-tests/build/diagnostics" + mkdir -p "$DIAG_DIR" + + capture_diag() { + local label="$1" + local prefix="$2" + echo "=== diag capture: $label ===" + { + echo "# label: $label" + echo "# t: $(date -u +%FT%TZ)" + echo + echo "## adb devices" + adb devices 2>&1 + echo + echo "## top activities" + adb shell dumpsys activity activities 2>&1 | head -200 + echo + echo "## window state (focused app + visible windows)" + adb shell dumpsys window 2>&1 | head -200 + echo + echo "## connectivity (active default network, link addresses, dns)" + adb shell dumpsys connectivity 2>&1 | head -120 + echo + echo "## meminfo (system_server + integration tests)" + adb shell dumpsys meminfo 2>&1 | head -80 + echo + echo "## installed iterable packages" + adb shell pm list packages 2>&1 | grep iterable || true + echo + echo "## boot props" + adb shell getprop sys.boot_completed dev.bootcomplete init.svc.netd net.dns1 ro.build.version.release ro.build.version.sdk 2>&1 | head -10 + } > "$DIAG_DIR/${prefix}-state.txt" 2>&1 + + # UiAutomator hierarchy and screenshot β€” the smoking gun for "button not found" + adb shell uiautomator dump /sdcard/hierarchy.xml >/dev/null 2>&1 || true + adb pull /sdcard/hierarchy.xml "$DIAG_DIR/${prefix}-hierarchy.xml" >/dev/null 2>&1 || echo "(no hierarchy)" + adb shell screencap -p /sdcard/screenshot.png >/dev/null 2>&1 || true + adb pull /sdcard/screenshot.png "$DIAG_DIR/${prefix}-screenshot.png" >/dev/null 2>&1 || echo "(no screenshot)" + } + 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 - + adb shell pm grant com.iterable.integration.tests android.permission.POST_NOTIFICATIONS || true + adb shell pm grant com.iterable.integration.tests android.permission.INTERNET || true + adb shell pm grant com.iterable.integration.tests android.permission.ACCESS_NETWORK_STATE || true + adb shell pm grant com.iterable.integration.tests android.permission.WAKE_LOCK || true + 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 & + + # Snapshot pre-test device state. If tests fail because the activity never + # foregrounded, this captures what was on screen *before* we even ran. + capture_diag "pre-test" "00-pre-test" + + # Stream full logcat to the workspace so we get an artifact even on success. + adb logcat -c >/dev/null 2>&1 || true + adb logcat > "$DIAG_DIR/10-logcat-full.txt" & LOGCAT_PID=$! - - # Run the specific test with better error handling + + GRADLE_EXIT=0 ./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 + --stacktrace --no-daemon || GRADLE_EXIT=$? + + # Stop logcat ASAP so post-test capture isn't entangled with it. kill $LOGCAT_PID 2>/dev/null || true + wait $LOGCAT_PID 2>/dev/null || true + + # Always capture post-test state β€” useful on success too (proves the test + # really did exercise the SDK and surface campaigns). + capture_diag "post-test" "20-post-test" + + # Filtered logcat for fast triage; full logcat is also uploaded. + grep -E 'IterableApi|IterableRequest|IterableInApp|IterableEmbedded|IntegrationMainActivity|BaseIntegrationTest|InAppMessageIntegrationTest|PushNotificationIntegrationTest|EmbeddedMessageIntegrationTest|DeepLinkIntegrationTest|FATAL|AndroidRuntime|ANR' \ + "$DIAG_DIR/10-logcat-full.txt" > "$DIAG_DIR/11-logcat-filtered.txt" 2>/dev/null || true + + echo + echo "=== diagnostics summary ===" + ls -la "$DIAG_DIR" + echo + echo "Last 50 logcat lines (full file uploaded as artifact):" + tail -50 "$DIAG_DIR/10-logcat-full.txt" || true + + if [ "$GRADLE_EXIT" != "0" ]; then + echo "::error::Test gradle task failed with exit code $GRADLE_EXIT β€” see e2e-diagnostics-api-${{ matrix.api-level }} artifact" + exit "$GRADLE_EXIT" + fi 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" + + - 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/ + integration-tests/build/outputs/ + if-no-files-found: warn + retention-days: 7 # test-summary: # name: Test Summary From b9f35617e9b7c20087cc14d398e567f695f866e0 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Tue, 5 May 2026 18:23:10 +0100 Subject: [PATCH 3/8] Fix Instrumentation tests failing in Build and Test workflow actions/setup-java@v1.4.3 was resolving java-version: 17 through the deprecated AdoptOpenJDK API. Between Apr 21 and Apr 22 that resolution shifted from 17.0.18 to 17.0.19, and the artifact returned for 17.0.19 makes sdkmanager exit immediately, breaking the emulator-runner action's Install Android SDK step on every run since. Upgrade setup-java to v4.8.0 with distribution: temurin so the JDK is pinned to a deterministic Adoptium Temurin build, and replace the deprecated gradle/wrapper-validation-action with gradle/actions/wrapper-validation@v4. Applied to all three jobs (check, unit-tests, instrumentation-tests). Co-authored-by: Cursor --- .github/workflows/build.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b072cdad2..9b3ccbf50 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -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 @@ -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 From c6aee2bc172f397f9cba036d7634d64fd1851950 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Wed, 6 May 2026 13:07:28 +0100 Subject: [PATCH 4/8] redact BCIT secrets from logs --- .github/workflows/inapp-e2e-tests.yml | 5 ++++- .../integration/tests/InAppMessageIntegrationTest.kt | 8 ++++---- .../com/iterable/integration/tests/MainActivity.kt | 12 ++++++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/inapp-e2e-tests.yml b/.github/workflows/inapp-e2e-tests.yml index 1a2d66ea1..73f8a429f 100644 --- a/.github/workflows/inapp-e2e-tests.yml +++ b/.github/workflows/inapp-e2e-tests.yml @@ -190,6 +190,10 @@ jobs: ITERABLE_SERVER_API_KEY: ${{ secrets.BCIT_ITERABLE_SERVER_API_KEY }} ITERABLE_TEST_USER_EMAIL: ${{ secrets.BCIT_ITERABLE_TEST_USER_EMAIL }} + # 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 @@ -198,7 +202,6 @@ jobs: path: | integration-tests/build/diagnostics/ integration-tests/build/reports/ - integration-tests/build/outputs/ if-no-files-found: warn retention-days: 7 diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt index 20d3fd7bf..a789cc01d 100644 --- a/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt @@ -130,10 +130,10 @@ class InAppMessageIntegrationTest : BaseIntegrationTest() { Assert.assertTrue("User should be signed in", userSignedIn) Log.d(TAG, "βœ… User signed in successfully: ${TestConstants.TEST_USER_EMAIL}") - // Step 2: Debug API key configuration - Log.d(TAG, "πŸ” Debug: ITERABLE_API_KEY = ${BuildConfig.ITERABLE_API_KEY}") - Log.d(TAG, "πŸ” Debug: ITERABLE_SERVER_API_KEY = ${BuildConfig.ITERABLE_SERVER_API_KEY}") - Log.d(TAG, "πŸ” Debug: ITERABLE_TEST_USER_EMAIL = ${BuildConfig.ITERABLE_TEST_USER_EMAIL}") + // SDK-170: log presence/length only (never values) β€” these end up in CI logcat artifacts. + Log.d(TAG, "API key configured: length=${BuildConfig.ITERABLE_API_KEY.length}") + Log.d(TAG, "Server API key configured: length=${BuildConfig.ITERABLE_SERVER_API_KEY.length}") + Log.d(TAG, "Test user email configured: length=${BuildConfig.ITERABLE_TEST_USER_EMAIL.length}") // Step 3: Try to trigger campaign via API (but don't fail if it doesn't work) Log.d(TAG, "🎯 Step 3: Attempting to trigger campaign via API...") diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt b/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt index 674985665..9b3d2faec 100644 --- a/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt +++ b/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt @@ -89,8 +89,16 @@ class MainActivity : AppCompatActivity() { } private fun setupUI() { - // Set API key text - findViewById(R.id.tvApiKey).text = "API Key: ${BuildConfig.ITERABLE_API_KEY}" + // SDK-170: never render the full API key into the view hierarchy β€” the integration + // tests CI captures hierarchy.xml and screenshot.png as artifacts on a public repo. + // Show only enough to confirm a non-empty key was loaded. + val apiKey = BuildConfig.ITERABLE_API_KEY + val keyDisplay = when { + apiKey.isEmpty() -> "API Key: (empty)" + apiKey.length < 8 -> "API Key: (length=${apiKey.length})" + else -> "API Key: ****${apiKey.takeLast(4)} (length=${apiKey.length})" + } + findViewById(R.id.tvApiKey).text = keyDisplay findViewById(R.id.btnPushNotifications).setOnClickListener { startActivity(Intent(this@MainActivity, PushNotificationTestActivity::class.java)) From f86eb7ce28734e99e6eff120cae0575221d930d9 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Wed, 6 May 2026 13:08:36 +0100 Subject: [PATCH 5/8] extract e2e diagnostics into run-e2e.sh --- .github/scripts/run-e2e.sh | 99 +++++++++++++++++++++++++ .github/workflows/inapp-e2e-tests.yml | 103 ++------------------------ 2 files changed, 104 insertions(+), 98 deletions(-) create mode 100755 .github/scripts/run-e2e.sh diff --git a/.github/scripts/run-e2e.sh b/.github/scripts/run-e2e.sh new file mode 100755 index 000000000..cc2dcfaf0 --- /dev/null +++ b/.github/scripts/run-e2e.sh @@ -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" diff --git a/.github/workflows/inapp-e2e-tests.yml b/.github/workflows/inapp-e2e-tests.yml index 73f8a429f..562c045e1 100644 --- a/.github/workflows/inapp-e2e-tests.yml +++ b/.github/workflows/inapp-e2e-tests.yml @@ -87,104 +87,11 @@ jobs: # Clean + start adb after platform-tools exist (avoids tcp:5037 noise) adb kill-server >/dev/null 2>&1 || true adb start-server - script: | - # SDK-170 OBSERVABILITY: capture diagnostics that survive the action exit. - # The android-emulator-runner kills the emulator on its way out, so any - # adb-dependent capture MUST happen before this script returns. We write - # everything under $GITHUB_WORKSPACE so a later step can upload it. - DIAG_DIR="$GITHUB_WORKSPACE/integration-tests/build/diagnostics" - mkdir -p "$DIAG_DIR" - - capture_diag() { - local label="$1" - local prefix="$2" - echo "=== diag capture: $label ===" - { - echo "# label: $label" - echo "# t: $(date -u +%FT%TZ)" - echo - echo "## adb devices" - adb devices 2>&1 - echo - echo "## top activities" - adb shell dumpsys activity activities 2>&1 | head -200 - echo - echo "## window state (focused app + visible windows)" - adb shell dumpsys window 2>&1 | head -200 - echo - echo "## connectivity (active default network, link addresses, dns)" - adb shell dumpsys connectivity 2>&1 | head -120 - echo - echo "## meminfo (system_server + integration tests)" - adb shell dumpsys meminfo 2>&1 | head -80 - echo - echo "## installed iterable packages" - adb shell pm list packages 2>&1 | grep iterable || true - echo - echo "## boot props" - adb shell getprop sys.boot_completed dev.bootcomplete init.svc.netd net.dns1 ro.build.version.release ro.build.version.sdk 2>&1 | head -10 - } > "$DIAG_DIR/${prefix}-state.txt" 2>&1 - - # UiAutomator hierarchy and screenshot β€” the smoking gun for "button not found" - adb shell uiautomator dump /sdcard/hierarchy.xml >/dev/null 2>&1 || true - adb pull /sdcard/hierarchy.xml "$DIAG_DIR/${prefix}-hierarchy.xml" >/dev/null 2>&1 || echo "(no hierarchy)" - adb shell screencap -p /sdcard/screenshot.png >/dev/null 2>&1 || true - adb pull /sdcard/screenshot.png "$DIAG_DIR/${prefix}-screenshot.png" >/dev/null 2>&1 || echo "(no screenshot)" - } - - echo "Emulator is ready! Running tests..." - echo "Setting up permissions..." - adb shell pm grant com.iterable.integration.tests android.permission.POST_NOTIFICATIONS || true - adb shell pm grant com.iterable.integration.tests android.permission.INTERNET || true - adb shell pm grant com.iterable.integration.tests android.permission.ACCESS_NETWORK_STATE || true - adb shell pm grant com.iterable.integration.tests android.permission.WAKE_LOCK || true - - 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" - - # Snapshot pre-test device state. If tests fail because the activity never - # foregrounded, this captures what was on screen *before* we even ran. - capture_diag "pre-test" "00-pre-test" - - # Stream full logcat to the workspace so we get an artifact even on success. - adb logcat -c >/dev/null 2>&1 || true - adb logcat > "$DIAG_DIR/10-logcat-full.txt" & - LOGCAT_PID=$! - - GRADLE_EXIT=0 - ./gradlew :integration-tests:connectedDebugAndroidTest \ - -Pandroid.testInstrumentationRunnerArguments.class=com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP \ - --stacktrace --no-daemon || GRADLE_EXIT=$? - - # Stop logcat ASAP so post-test capture isn't entangled with it. - kill $LOGCAT_PID 2>/dev/null || true - wait $LOGCAT_PID 2>/dev/null || true - - # Always capture post-test state β€” useful on success too (proves the test - # really did exercise the SDK and surface campaigns). - capture_diag "post-test" "20-post-test" - - # Filtered logcat for fast triage; full logcat is also uploaded. - grep -E 'IterableApi|IterableRequest|IterableInApp|IterableEmbedded|IntegrationMainActivity|BaseIntegrationTest|InAppMessageIntegrationTest|PushNotificationIntegrationTest|EmbeddedMessageIntegrationTest|DeepLinkIntegrationTest|FATAL|AndroidRuntime|ANR' \ - "$DIAG_DIR/10-logcat-full.txt" > "$DIAG_DIR/11-logcat-filtered.txt" 2>/dev/null || true - - echo - echo "=== diagnostics summary ===" - ls -la "$DIAG_DIR" - echo - echo "Last 50 logcat lines (full file uploaded as artifact):" - tail -50 "$DIAG_DIR/10-logcat-full.txt" || true - - if [ "$GRADLE_EXIT" != "0" ]; then - echo "::error::Test gradle task failed with exit code $GRADLE_EXIT β€” see e2e-diagnostics-api-${{ matrix.api-level }} artifact" - exit "$GRADLE_EXIT" - fi + # 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 }} From cdcaa0d0ade6d6c1b81e9600945bb8c79880424c Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Fri, 8 May 2026 11:57:06 +0100 Subject: [PATCH 6/8] using correct find object way for the in app test --- .../tests/InAppMessageIntegrationTest.kt | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt index a789cc01d..c5026915d 100644 --- a/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt @@ -11,9 +11,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry import androidx.test.runner.lifecycle.Stage -import androidx.test.uiautomator.UiDevice -import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until import com.iterable.iterableapi.IterableApi import com.iterable.iterableapi.IterableInAppMessage import com.iterable.iterableapi.IterableInAppLocation @@ -38,6 +38,10 @@ class InAppMessageIntegrationTest : BaseIntegrationTest() { private const val TAG = "InAppMessageIntegrationTest" private const val TEST_CAMPAIGN_ID = TestConstants.TEST_INAPP_CAMPAIGN_ID private const val TEST_EVENT_NAME = "test_inapp_event" + + // SDK-170: ample wait so a slow CI emulator (SurfaceSyncGroup timeouts in logs) can + // finish drawing MainActivity before we look for the button. Local cost: 0 (passes fast). + private const val BTN_IN_APP_TIMEOUT_MS = 30_000L } private lateinit var uiDevice: UiDevice @@ -100,16 +104,22 @@ class InAppMessageIntegrationTest : BaseIntegrationTest() { Log.d(TAG, "πŸ”§ MainActivity is ready!") - // Step 2: Click the "In-App Messages" button to navigate to InAppMessageTestActivity - Log.d(TAG, "πŸ”§ Step 2: Clicking 'In-App Messages' button...") - val inAppButton = uiDevice.findObject(UiSelector().resourceId("com.iterable.integration.tests:id/btnInAppMessages")) - if (inAppButton.exists()) { + // Step 2: Click the "In-App Messages" button to navigate to InAppMessageTestActivity. + // SDK-170: Lifecycle.RESUMED does not guarantee the layout is drawn β€” under CI load + // (SurfaceSyncGroup transaction-ready timeouts), `findObject(...).exists()` ran ~2s + // after RESUMED and returned false because the button view was not yet present in the + // accessibility tree. `uiDevice.wait(Until.findObject(...))` polls until the node + // appears or the timeout elapses. + Log.d(TAG, "πŸ”§ Step 2: Waiting for and clicking 'In-App Messages' button...") + val inAppButton = uiDevice.wait( + Until.findObject(By.res("com.iterable.integration.tests", "btnInAppMessages")), + BTN_IN_APP_TIMEOUT_MS + ) + if (inAppButton != null) { inAppButton.click() Log.d(TAG, "πŸ”§ Clicked In-App Messages button successfully") } else { - //Take screenshot for debugging -// uiDevice.takeScreenshot(File("/sdcard/Download/InAppButtonNotFound.png")) - Log.e(TAG, "❌ In-App Messages button not found!") + Log.e(TAG, "❌ In-App Messages button not found within ${BTN_IN_APP_TIMEOUT_MS}ms (current package: ${uiDevice.currentPackageName})") Assert.fail("In-App Messages button not found in MainActivity") } From 46709d82fefe995238a73a6dbd5aee5c34fc05ec Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Fri, 8 May 2026 15:01:49 +0100 Subject: [PATCH 7/8] Drop local-only trace harness and printBuildConfig task scripts/trace-bcit.sh and the integration-tests printBuildConfig Gradle task were used for local replication while diagnosing SDK-170. They are not invoked by CI. Removing them keeps the PR diff focused on what actually fixes the in-app E2E job. Co-authored-by: Cursor --- integration-tests/build.gradle | 30 -- scripts/trace-bcit.sh | 526 --------------------------------- 2 files changed, 556 deletions(-) delete mode 100755 scripts/trace-bcit.sh diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index edb8f04d8..4ef5616a2 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -119,36 +119,6 @@ dependencies { androidTestImplementation 'com.google.code.gson:gson:2.8.9' } -// SDK-170 β€” debug task that surfaces the build-time BuildConfig inputs the test APK was -// built with. We only print *length* and *first 4 chars* of secrets so they never appear -// in CI logs verbatim. Used by scripts/trace-bcit.sh. -tasks.register('printBuildConfig') { - group = 'verification' - description = 'Print the integration-test BuildConfig inputs as seen by the build (length-only for secrets).' - doLast { - def localProperties = new Properties() - def localPropertiesFile = rootProject.file('local.properties') - if (localPropertiesFile.exists()) { - localPropertiesFile.withInputStream { localProperties.load(it) } - } - def showPrefix = System.getenv('TRACE_SHOW_PREFIX') == '1' - def report = { String name -> - def fromLocal = localProperties.getProperty(name) - def fromEnv = System.getenv(name) - def chosen = fromLocal ?: fromEnv ?: '' - def source = fromLocal ? 'local.properties' : (fromEnv ? 'env' : 'default-fallback') - def line = "${name} length=${chosen.length()} source=${source}" - if (showPrefix && chosen.length() >= 4) { - line += " prefix=${chosen.substring(0, 4)}\u2026" - } - println line - } - report('ITERABLE_API_KEY') - report('ITERABLE_SERVER_API_KEY') - report('ITERABLE_TEST_USER_EMAIL') - } -} - // Jacoco coverage for integration tests tasks.withType(Test) { jacoco.includeNoLocationClasses = true diff --git a/scripts/trace-bcit.sh b/scripts/trace-bcit.sh deleted file mode 100755 index c7f185256..000000000 --- a/scripts/trace-bcit.sh +++ /dev/null @@ -1,526 +0,0 @@ -#!/usr/bin/env bash -# trace-bcit.sh β€” End-to-end path tracing harness for SDK-170. -# -# Captures 10 checkpoints along the path: -# β‘  emulator subprocess -# β‘‘ hostβ†’guest network bring-up -# β‘’ guest network state (ConnectivityService, netd, wifi) -# β‘£ DNS reachability of api.iterable.com -# β‘€ TCP/TLS reachability of api.iterable.com:443 -# β‘₯ build-time inputs (BuildConfig.ITERABLE_API_KEY length only β€” never the value) -# ⑦ SDK init order -# β‘§ SDK HTTP request lifecycle (logcat) -# ⑨ test outcome (junit reports) -# β‘© device screenshot at end -# -# Designed to run both locally (macOS) and inside a GitHub Actions step -# wrapped by ReactiveCircus/android-emulator-runner. When the env var -# TRACE_MODE=ci-script is set, the script assumes the emulator is already -# running (the action launched it) and skips its own emulator lifecycle. -# -# All output is written to a single timestamped folder under TRACE_OUT_ROOT -# (default: /tmp). Nothing is written outside the repo or that folder. -# -# Exit codes: -# 0 β€” full trace completed (test may have passed or failed; see 08-summary.md) -# 2 β€” fatal setup error (e.g. emulator wouldn't boot, can't write artifacts) - -set -uo pipefail - -# ------------------------------------------------------------------------------ -# Config -# ------------------------------------------------------------------------------ - -readonly REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -readonly TRACE_MODE="${TRACE_MODE:-local}" # local | ci-script -readonly API_LEVEL="${API_LEVEL:-34}" -readonly TARGET="${TARGET:-google_apis}" -readonly ARCH="${ARCH:-arm64-v8a}" # arm64-v8a locally, x86_64 in CI -readonly AVD_NAME="${AVD_NAME:-bcit_trace_avd}" -readonly EMULATOR_PORT="${EMULATOR_PORT:-5554}" -readonly EMULATOR_SERIAL="emulator-${EMULATOR_PORT}" -readonly TEST_CLASS="com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP" -readonly TRACE_OUT_ROOT="${TRACE_OUT_ROOT:-/tmp}" -readonly TIMESTAMP="$(date +%Y%m%d-%H%M%S)" -readonly TRACE_DIR="${TRACE_OUT_ROOT}/sdk-170-trace-${TIMESTAMP}" - -# Probe targets -readonly TARGET_HOST="api.iterable.com" -readonly TARGET_PORT="443" - -# ------------------------------------------------------------------------------ -# Logging -# ------------------------------------------------------------------------------ - -log() { printf '\033[1;34m[trace]\033[0m %s\n' "$*"; } -warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*" >&2; } -err() { printf '\033[1;31m[err]\033[0m %s\n' "$*" >&2; } -die() { err "$*"; exit 2; } - -run_step() { - # run_step -- - # Records the command, captures stdout+stderr, never aborts the harness. - local name="$1"; shift - local out="$1"; shift - [[ "$1" == "--" ]] && shift - printf '# %s\n# cmd: %s\n# t: %s\n\n' "$name" "$*" "$(date -u +%FT%TZ)" > "$out" - if "$@" >> "$out" 2>&1; then - log " βœ“ ${name}" - else - warn " βœ— ${name} (exit $?). See ${out#"${TRACE_DIR}/"}" - fi -} - -adb_shell() { - "$ADB" -s "${EMULATOR_SERIAL}" shell "$@" -} - -# ------------------------------------------------------------------------------ -# Tool resolution -# ------------------------------------------------------------------------------ - -find_tools() { - local sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-${HOME}/Library/Android/sdk}}" - [[ -d "$sdk" ]] || die "ANDROID_HOME not set and ${sdk} doesn't exist" - - EMULATOR="${sdk}/emulator/emulator" - ADB="${sdk}/platform-tools/adb" - AVDMANAGER="${sdk}/cmdline-tools/latest/bin/avdmanager" - SDKMANAGER="${sdk}/cmdline-tools/latest/bin/sdkmanager" - - for tool in "$EMULATOR" "$ADB"; do - [[ -x "$tool" ]] || die "missing tool: $tool" - done - if [[ "$TRACE_MODE" == "local" ]]; then - [[ -x "$AVDMANAGER" ]] || warn "avdmanager not at $AVDMANAGER (local mode needs it)" - [[ -x "$SDKMANAGER" ]] || warn "sdkmanager not at $SDKMANAGER (local mode needs it)" - fi - - log "ANDROID_HOME = $sdk" - log "emulator = $("$EMULATOR" -version 2>&1 | head -1)" - log "adb = $("$ADB" version | head -1)" -} - -# ------------------------------------------------------------------------------ -# Step 0 β€” environment snapshot -# ------------------------------------------------------------------------------ - -step_00_env() { - log "Step 0 β€” environment snapshot" - local out="${TRACE_DIR}/00-env.txt" - { - echo "# Trace mode: $TRACE_MODE" - echo "# Trace dir: $TRACE_DIR" - echo "# Timestamp: $TIMESTAMP" - echo - echo "## Host" - echo "uname -a: $(uname -a)" - if command -v sw_vers >/dev/null 2>&1; then - echo "sw_vers: $(sw_vers -productName) $(sw_vers -productVersion)" - fi - echo "arch: $(uname -m)" - echo "user: ${USER:-?}" - echo "shell: ${SHELL:-?}" - echo - echo "## Android SDK" - echo "ANDROID_HOME=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-${HOME}/Library/Android/sdk}}" - "$EMULATOR" -version 2>&1 | head -2 - "$ADB" version | head -2 - echo - echo "## Trace target" - echo "API_LEVEL=$API_LEVEL TARGET=$TARGET ARCH=$ARCH" - echo "AVD=$AVD_NAME PORT=$EMULATOR_PORT" - echo "TEST_CLASS=$TEST_CLASS" - echo - echo "## CI variables (presence only)" - for v in GITHUB_ACTIONS GITHUB_RUN_ID GITHUB_SHA RUNNER_OS RUNNER_ARCH; do - eval "val=\${$v:-}" - printf '%s=%s\n' "$v" "${val:+SET}" - done - echo - echo "## Iterable secrets (presence + length only)" - for v in ITERABLE_API_KEY ITERABLE_SERVER_API_KEY ITERABLE_TEST_USER_EMAIL; do - eval "val=\${$v:-}" - printf '%-32s present=%s length=%s\n' "$v" "$([[ -n "$val" ]] && echo yes || echo no)" "${#val}" - done - } > "$out" -} - -# ------------------------------------------------------------------------------ -# Step 1 β€” emulator launch (local only; in CI the action handles it) -# ------------------------------------------------------------------------------ - -ensure_system_image() { - local pkg="system-images;android-${API_LEVEL};${TARGET};${ARCH}" - if "$SDKMANAGER" --list_installed 2>/dev/null | grep -q "$pkg"; then - log "system image already installed: $pkg" - return 0 - fi - log "installing system image: $pkg (this may take a few minutes)" - yes | "$SDKMANAGER" --licenses >/dev/null 2>&1 || true - "$SDKMANAGER" "$pkg" 2>&1 | tail -5 || die "failed to install $pkg" -} - -ensure_avd() { - if "$EMULATOR" -list-avds 2>/dev/null | grep -qx "$AVD_NAME"; then - log "AVD already exists: $AVD_NAME (deleting for clean baseline)" - "$AVDMANAGER" delete avd -n "$AVD_NAME" >/dev/null 2>&1 || true - fi - local pkg="system-images;android-${API_LEVEL};${TARGET};${ARCH}" - log "creating AVD: $AVD_NAME ($pkg, profile=pixel_6)" - echo "no" | "$AVDMANAGER" create avd \ - -n "$AVD_NAME" \ - -k "$pkg" \ - -d "pixel_6" \ - --force >/dev/null - # Match CI sizing: ram-size 3072M, heap-size 576M (set in config.ini). - local cfg="${HOME}/.android/avd/${AVD_NAME}.avd/config.ini" - if [[ -f "$cfg" ]]; then - { - echo "hw.ramSize=3072" - echo "vm.heapSize=576" - echo "disk.dataPartition.size=6000M" - echo "hw.keyboard=yes" - } >> "$cfg" - fi -} - -step_01_launch_emulator() { - if [[ "$TRACE_MODE" != "local" ]]; then - log "Step 1 β€” emulator already running (CI mode)" - return 0 - fi - log "Step 1 β€” launching emulator" - ensure_system_image - ensure_avd - local emu_log="${TRACE_DIR}/01-emulator.log" - # CI flags: -no-window -no-snapshot -gpu swiftshader_indirect -no-boot-anim -camera-back none -partition-size 6000 - nohup "$EMULATOR" \ - -avd "$AVD_NAME" \ - -port "$EMULATOR_PORT" \ - -no-window \ - -no-snapshot \ - -no-boot-anim \ - -gpu swiftshader_indirect \ - -camera-back none \ - -partition-size 6000 \ - -no-audio \ - >"$emu_log" 2>&1 & - EMU_PID=$! - echo "$EMU_PID" > "${TRACE_DIR}/.emulator.pid" - log " emulator PID=$EMU_PID, port=$EMULATOR_PORT, log=01-emulator.log" - trap 'cleanup' EXIT -} - -cleanup() { - if [[ -f "${TRACE_DIR}/.emulator.pid" ]]; then - local pid - pid="$(cat "${TRACE_DIR}/.emulator.pid")" - log "stopping emulator PID=$pid" - "$ADB" -s "${EMULATOR_SERIAL}" emu kill 2>/dev/null || true - sleep 2 - kill "$pid" 2>/dev/null || true - fi -} - -# ------------------------------------------------------------------------------ -# Step 2 β€” boot -# ------------------------------------------------------------------------------ - -step_02_boot() { - log "Step 2 β€” wait for boot" - mkdir -p "${TRACE_DIR}/02-boot" - local boot_log="${TRACE_DIR}/02-boot/poll.txt" - : > "$boot_log" - - log " waiting for adb to detect device (timeout 90s)..." - local t=0 - while ! "$ADB" devices | grep -q "${EMULATOR_SERIAL}.*device"; do - sleep 2; t=$((t+2)) - [[ $t -ge 90 ]] && { warn " device not detected after 90s"; "$ADB" devices >> "$boot_log"; return 1; } - done - - log " device detected, polling sys.boot_completed (timeout 300s)..." - t=0 - while :; do - local boot trace - boot="$(adb_shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" - trace="$(adb_shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" - printf 't=%ds sys.boot_completed=%q dev.bootcomplete=%q\n' "$t" "$boot" "$trace" >> "$boot_log" - [[ "$boot" == "1" ]] && break - sleep 3; t=$((t+3)) - if [[ $t -ge 300 ]]; then - warn " boot timeout after 300s" - adb_shell logcat -d -t 200 > "${TRACE_DIR}/02-boot/logcat-during-boot.txt" 2>&1 || true - return 1 - fi - done - - log " booted at t=${t}s" - echo "boot_seconds=${t}" >> "$boot_log" - # Match CI: disable animations - for k in window_animation_scale transition_animation_scale animator_duration_scale; do - adb_shell settings put global "$k" 0 || true - done - - # SDK-170: sys.boot_completed=1 is NOT a sufficient signal β€” Wi-Fi association - # via the virtio-wifi/netsim stack lags boot by 20-90s. Poll for an active - # default network and reachable internet before we declare the emulator ready. - log " waiting for default network (timeout 180s)..." - local n=0 - local default_net="none" - while [[ "$default_net" == "none" ]]; do - default_net="$(adb_shell 'dumpsys connectivity | grep -E "^Active default network" | head -1' 2>/dev/null | sed 's/.*: //;s/[[:space:]]*$//' | tr -d '\r')" - printf 't=%ds active_default_network=%q\n' "$n" "$default_net" >> "$boot_log" - [[ "$default_net" != "none" && -n "$default_net" ]] && break - sleep 3; n=$((n+3)) - if [[ $n -ge 180 ]]; then - warn " default network never came up within 180s" - echo "default_network_timeout=true" >> "$boot_log" - break - fi - done - log " default network up at t=${n}s after boot (default=${default_net})" - echo "default_network_seconds=${n}" >> "$boot_log" - - log " waiting for internet reachability (ping 8.8.8.8 succeeds; timeout 60s)..." - local p=0 - while ! adb_shell 'ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1'; do - sleep 2; p=$((p+2)) - if [[ $p -ge 60 ]]; then - warn " internet unreachable after 60s of polling" - echo "internet_timeout=true" >> "$boot_log" - return 0 - fi - done - log " internet reachable at t=${p}s after default network" - echo "internet_seconds=${p}" >> "$boot_log" - return 0 -} - -# ------------------------------------------------------------------------------ -# Step 3 β€” guest network state -# ------------------------------------------------------------------------------ - -step_03_guest_network() { - log "Step 3 β€” guest network state" - mkdir -p "${TRACE_DIR}/03-network-guest" - local d="${TRACE_DIR}/03-network-guest" - - run_step "ifconfig" "$d/ifconfig.txt" -- adb_shell ifconfig - run_step "ip-route" "$d/ip-route.txt" -- adb_shell ip route - run_step "ip-addr" "$d/ip-addr.txt" -- adb_shell ip addr - run_step "getprop-net" "$d/getprop-net.txt" -- adb_shell "getprop | grep -E 'net\\.|wifi\\.'" - run_step "connectivity" "$d/connectivity.txt" -- adb_shell dumpsys connectivity - run_step "wifi" "$d/wifi.txt" -- adb_shell dumpsys wifi - run_step "netd-resolver" "$d/netd.txt" -- adb_shell ndc resolver dump - run_step "init.svc.netd" "$d/init-netd.txt" -- adb_shell getprop init.svc.netd -} - -# ------------------------------------------------------------------------------ -# Step 4 β€” DNS + Step 5 β€” TCP/TLS reachability of api.iterable.com -# ------------------------------------------------------------------------------ - -step_04_dns_tcp() { - log "Step 4+5 β€” DNS / TCP / TLS reachability of ${TARGET_HOST}" - mkdir -p "${TRACE_DIR}/04-dns-tcp" - local d="${TRACE_DIR}/04-dns-tcp" - - run_step "ping-iterable" "$d/ping.txt" -- adb_shell ping -c 5 -W 3 "$TARGET_HOST" - run_step "ping-8.8.8.8" "$d/ping-dns.txt" -- adb_shell ping -c 3 -W 3 8.8.8.8 - run_step "nslookup" "$d/nslookup.txt" -- adb_shell nslookup "$TARGET_HOST" - run_step "getent-hosts" "$d/getent.txt" -- adb_shell getent hosts "$TARGET_HOST" - - # Java-level probe via a tiny inline script that uses the SDK's classloader. - # We skip that here and rely on the test step (07/08) to surface in-app HTTP. - # Instead, run a host-side curl so we know whether the network exit is reachable - # from the runner (CI baseline). - run_step "host-curl-iterable" "$d/host-curl.txt" -- bash -c \ - "curl -sS -o /dev/null -w 'http=%{http_code}\\ntcp=%{time_connect}\\ntls=%{time_appconnect}\\ntotal=%{time_total}\\nremoteip=%{remote_ip}\\n' --max-time 10 'https://${TARGET_HOST}/api/inApp/getMessages?email=trace@iterable.com'" -} - -# ------------------------------------------------------------------------------ -# Step 6 β€” build with diagnostics -# ------------------------------------------------------------------------------ - -step_06_build() { - log "Step 6 β€” build APK + capture BuildConfig (key length only)" - mkdir -p "${TRACE_DIR}/05-apk" - local d="${TRACE_DIR}/05-apk" - - pushd "$REPO_ROOT" >/dev/null - run_step "gradle-version" "$d/gradle-version.txt" -- ./gradlew --version - run_step "gradle-printBuildConfig" "$d/printBuildConfig.txt" -- \ - ./gradlew :integration-tests:printBuildConfig -q - run_step "gradle-assemble" "$d/gradle-assemble.txt" -- \ - ./gradlew :integration-tests:assembleDebug :integration-tests:assembleDebugAndroidTest --no-daemon - popd >/dev/null - - # APK introspection - local apk - apk="$(ls -t "$REPO_ROOT"/integration-tests/build/outputs/apk/debug/*.apk 2>/dev/null | head -1 || true)" - if [[ -n "$apk" ]]; then - cp "$apk" "$d/installed.apk" 2>/dev/null || true - if command -v aapt2 >/dev/null 2>&1; then - run_step "aapt2-dump" "$d/aapt2-dump.txt" -- aapt2 dump badging "$apk" - elif command -v aapt >/dev/null 2>&1; then - run_step "aapt-dump" "$d/aapt-dump.txt" -- aapt dump badging "$apk" - fi - else - warn " APK not found after build" - fi -} - -# ------------------------------------------------------------------------------ -# Step 7+8+9+10 β€” install, run test, capture -# ------------------------------------------------------------------------------ - -step_07_install_run() { - log "Step 7-9 β€” install + run instrumented test" - mkdir -p "${TRACE_DIR}/06-test-run/junit-reports" - mkdir -p "${TRACE_DIR}/06-test-run/screenshots" - mkdir -p "${TRACE_DIR}/07-logcat" - local d="${TRACE_DIR}/06-test-run" - - # Start logcat in background - local logcat_pid_file="${TRACE_DIR}/.logcat.pid" - "$ADB" -s "${EMULATOR_SERIAL}" logcat -c >/dev/null 2>&1 || true - "$ADB" -s "${EMULATOR_SERIAL}" logcat > "${TRACE_DIR}/07-logcat/full.txt" 2>&1 & - echo $! > "$logcat_pid_file" - - # Pre-test screenshot - adb_shell screencap -p /sdcard/pre-test.png >/dev/null 2>&1 || true - "$ADB" -s "${EMULATOR_SERIAL}" pull /sdcard/pre-test.png "$d/screenshots/pre-test.png" >/dev/null 2>&1 || true - - pushd "$REPO_ROOT" >/dev/null - log " running test: ${TEST_CLASS}" - ./gradlew :integration-tests:connectedDebugAndroidTest \ - -Pandroid.testInstrumentationRunnerArguments.class="${TEST_CLASS}" \ - --stacktrace --no-daemon \ - > "$d/gradle.log" 2>&1 - local rc=$? - echo "$rc" > "$d/gradle.exit-code" - log " test gradle exit code: $rc" - popd >/dev/null - - # Post-test screenshot - adb_shell screencap -p /sdcard/post-test.png >/dev/null 2>&1 || true - "$ADB" -s "${EMULATOR_SERIAL}" pull /sdcard/post-test.png "$d/screenshots/post-test.png" >/dev/null 2>&1 || true - - # Grab the UiAutomator hierarchy dump if the test left one behind (SDK-170 diag) - "$ADB" -s "${EMULATOR_SERIAL}" pull /sdcard/Download/uiautomator-after-launch.xml \ - "$d/uiautomator-after-launch.xml" >/dev/null 2>&1 || true - - # Also dump current window hierarchy for post-mortem - adb_shell uiautomator dump /sdcard/post-test-hierarchy.xml >/dev/null 2>&1 || true - "$ADB" -s "${EMULATOR_SERIAL}" pull /sdcard/post-test-hierarchy.xml \ - "$d/post-test-hierarchy.xml" >/dev/null 2>&1 || true - - # Stop logcat - if [[ -f "$logcat_pid_file" ]]; then - kill "$(cat "$logcat_pid_file")" 2>/dev/null || true - fi - - # Copy junit reports - cp -R "$REPO_ROOT/integration-tests/build/reports/androidTests/connected/." \ - "$d/junit-reports/" 2>/dev/null || true - - # Filtered logcats - grep -E 'IterableApi|IterableRequest|IterableInApp|IterableEmbedded|IntegrationMainActivity|BaseIntegrationTest' \ - "${TRACE_DIR}/07-logcat/full.txt" > "${TRACE_DIR}/07-logcat/iterable.txt" 2>/dev/null || true - grep -E 'AndroidRuntime|FATAL|ANR|SIGSEGV|System\.err' \ - "${TRACE_DIR}/07-logcat/full.txt" > "${TRACE_DIR}/07-logcat/crashes.txt" 2>/dev/null || true - - return 0 -} - -# ------------------------------------------------------------------------------ -# Step 11 β€” summary -# ------------------------------------------------------------------------------ - -# helper: print first non-empty grep match or "β€”" -peek() { grep -m1 -E "$1" "$2" 2>/dev/null | sed 's/[[:space:]]\+$//' || echo "β€”"; } - -step_99_summary() { - log "Writing 08-summary.md" - local s="${TRACE_DIR}/08-summary.md" - local d_net="${TRACE_DIR}/03-network-guest" - local d_dns="${TRACE_DIR}/04-dns-tcp" - local d_apk="${TRACE_DIR}/05-apk" - local d_test="${TRACE_DIR}/06-test-run" - local d_log="${TRACE_DIR}/07-logcat" - - local boot_seconds default_net_seconds internet_seconds - boot_seconds="$(grep '^boot_seconds=' "${TRACE_DIR}/02-boot/poll.txt" 2>/dev/null | cut -d= -f2 || echo '?')" - default_net_seconds="$(grep '^default_network_seconds=' "${TRACE_DIR}/02-boot/poll.txt" 2>/dev/null | cut -d= -f2 || echo '?')" - internet_seconds="$(grep '^internet_seconds=' "${TRACE_DIR}/02-boot/poll.txt" 2>/dev/null | cut -d= -f2 || echo '?')" - local key_len - key_len="$(grep -E 'ITERABLE_API_KEY' "$d_apk/printBuildConfig.txt" 2>/dev/null | head -1 || echo 'β€”')" - local ping_loss - ping_loss="$(grep -Eo '[0-9]+% packet loss' "$d_dns/ping.txt" 2>/dev/null | head -1 || echo 'β€”')" - local host_http - host_http="$(grep -E '^http=' "$d_dns/host-curl.txt" 2>/dev/null | head -1 || echo 'β€”')" - local test_rc - test_rc="$(cat "$d_test/gradle.exit-code" 2>/dev/null || echo '?')" - - { - echo "# SDK-170 trace summary" - echo - echo "- timestamp: \`${TIMESTAMP}\`" - echo "- mode: \`${TRACE_MODE}\`" - echo "- arch: \`$(uname -m)\` API \`${API_LEVEL}\` ${TARGET}/${ARCH}" - echo - echo "## Checkpoints" - echo - echo "| # | Checkpoint | Result |" - echo "|---|---------------------------------------------|--------|" - echo "| β‘  | emulator launched | $([[ -s "${TRACE_DIR}/01-emulator.log" || "$TRACE_MODE" == ci-script ]] && echo PASS || echo FAIL) |" - echo "| β‘‘ | boot completed (sys.boot_completed=1) | ${boot_seconds}s |" - echo "| β‘‘ | default network up (after boot) | +${default_net_seconds}s |" - echo "| β‘‘ | internet reachable (ping 8.8.8.8) | +${internet_seconds}s |" - echo "| β‘’ | wlan0 has IP | $(peek '^[[:space:]]*inet ' "$d_net/ifconfig.txt") |" - echo "| β‘’ | default route | $(peek 'default' "$d_net/ip-route.txt") |" - echo "| β‘’ | net.dns1 | $(peek '^net\\.dns1' "$d_net/getprop-net.txt") |" - echo "| β‘£ | DNS api.iterable.com | $(peek 'address|server can' "$d_dns/nslookup.txt") |" - echo "| β‘€ | ping api.iterable.com loss | ${ping_loss} |" - echo "| β‘€ | host curl https://api.iterable.com | ${host_http} |" - echo "| β‘₯ | BuildConfig key | ${key_len} |" - echo "| ⑦ | SDK init in logcat | $([[ -s "$d_log/iterable.txt" ]] && grep -m1 -c 'Iterable SDK initialized' "$d_log/iterable.txt" || echo 0) hits |" - echo "| β‘§ | iterable HTTP req in logcat | $([[ -s "$d_log/iterable.txt" ]] && grep -c -E 'Sending request|response code' "$d_log/iterable.txt" || echo 0) hits |" - echo "| ⑨ | gradle test exit code | ${test_rc} |" - echo "| β‘© | post-test screenshot | $([[ -f "$d_test/screenshots/post-test.png" ]] && echo present || echo missing) |" - echo - echo "## Top crashes (if any)" - echo - echo '```' - head -30 "$d_log/crashes.txt" 2>/dev/null || echo "(none)" - echo '```' - echo - echo "## Files" - echo - find "$TRACE_DIR" -maxdepth 2 -type f | sed "s|${TRACE_DIR}/||" | sort - } > "$s" - log "summary written: $s" -} - -# ------------------------------------------------------------------------------ -# Main -# ------------------------------------------------------------------------------ - -main() { - mkdir -p "$TRACE_DIR" || die "cannot create $TRACE_DIR" - log "Trace output: $TRACE_DIR" - find_tools - step_00_env - step_01_launch_emulator || die "emulator launch failed" - step_02_boot || warn "boot incomplete; continuing for partial trace" - step_03_guest_network - step_04_dns_tcp - step_06_build - step_07_install_run - step_99_summary - log "Done. Trace: $TRACE_DIR" - log " open ${TRACE_DIR}/08-summary.md" -} - -main "$@" From 7d4a80baeea5baa0ec6c268dc45dd67cace36904 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Fri, 8 May 2026 15:02:02 +0100 Subject: [PATCH 8/8] Migrate in-app E2E job to ubuntu-latest with KVM acceleration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous CI run on macos-15-intel (2 cores / 3GB AVD on HVF) starved system_server during cold boot. Logcat shows continuous ANRs in systemui, nexuslauncher, gms.persistent, googlequicksearchbox, com.android.phone, com.google.android.as, keychain, permissioncontroller β€” all before and during the test. UiAutomator's wait then timed out with current package = 'android' (a system dialog) sitting on top of MainActivity, so btnInAppMessages was unreachable. Switching to ubuntu-latest with KVM acceleration gives the AVD real hardware virtualization on a 4 vCPU / 16GB host, stopping the ANR storm and letting the test see MainActivity within its 30s wait. Co-authored-by: Cursor --- .github/workflows/inapp-e2e-tests.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/inapp-e2e-tests.yml b/.github/workflows/inapp-e2e-tests.yml index 562c045e1..b5cd28d29 100644 --- a/.github/workflows/inapp-e2e-tests.yml +++ b/.github/workflows/inapp-e2e-tests.yml @@ -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: @@ -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