Files
gitea-migration/runners-conversion/periodVault/run-emulator-tests.sh

539 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# run-emulator-tests.sh — Run all emulator/simulator UI tests for PeriodVault
# Usage: ./scripts/run-emulator-tests.sh [android|ios|all]
# Logs to build/emulator-tests.log; script reads the log to detect adb errors (e.g. multiple devices).
#
# iOS watchdog env controls:
# IOS_HEARTBEAT_SECONDS (default: 30)
# IOS_STARTUP_PROGRESS_TIMEOUT_SECONDS (default: 900)
# IOS_TEST_STALL_TIMEOUT_SECONDS (default: 480)
# IOS_UNRESPONSIVE_STALL_TIMEOUT_SECONDS(default: 120)
# IOS_HARD_TIMEOUT_SECONDS (default: 10800)
# IOS_ACTIVE_CPU_THRESHOLD (default: 1.0)
set -euo pipefail
PLATFORM="${1:-all}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT"
# shellcheck source=scripts/lib.sh
source "$SCRIPT_DIR/lib.sh"
ensure_log_file "emulator-tests.log"
# Start Android emulator headless for test runs (no GUI window needed)
export EMULATOR_HEADLESS=1
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ANDROID_PASS=0
IOS_PASS=0
ANDROID_FAIL=0
IOS_FAIL=0
run_android() {
echo -e "${YELLOW}=== Android Emulator Tests ===${NC}"
if ! ensure_android_emulator; then
echo -e "${RED}ERROR: Could not start or connect to Android emulator. See $LOG_FILE${NC}"
ANDROID_FAIL=1
return 1
fi
# Disable animations for stable UI tests
run_and_log "adb_disable_animations" adb shell "settings put global window_animation_scale 0; settings put global transition_animation_scale 0; settings put global animator_duration_scale 0" || true
# Pre-flight: verify emulator is responsive via adb shell
echo "Verifying Android emulator is responsive..."
if ! adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
echo -e "${RED}ERROR: Android emulator not responsive (sys.boot_completed != 1). Aborting.${NC}"
ANDROID_FAIL=1
return 1
fi
echo "Android emulator is responsive."
# Uninstall the app to ensure a clean database for tests
echo "Cleaning app data..."
adb uninstall periodvault.androidApp 2>/dev/null || true
adb uninstall periodvault.androidApp.test 2>/dev/null || true
echo "Running Android instrumented tests..."
local GRADLE_PID
local GRADLE_EXIT=0
local TOTAL_ANDROID_TESTS=0
TOTAL_ANDROID_TESTS=$(find androidApp/src/androidTest -name '*.kt' -type f -exec grep -hE '@Test' {} + 2>/dev/null | wc -l | tr -d ' ')
if [[ -z "$TOTAL_ANDROID_TESTS" ]]; then
TOTAL_ANDROID_TESTS=0
fi
./gradlew androidApp:connectedDebugAndroidTest 2>&1 &
GRADLE_PID=$!
# Progress/liveness watchdog:
# - emits heartbeat every 30s with completed Android test cases and emulator health
# - kills early only if emulator is unresponsive and test progress is stalled for 10m
# - retains a generous hard timeout as last-resort safety net
local HEARTBEAT_SECONDS=30
local UNRESPONSIVE_STALL_TIMEOUT_SECONDS=600
local HARD_TIMEOUT_SECONDS=7200 # 2 hours
(
local start_ts now_ts elapsed
local last_progress_ts
local completed=0
local last_completed=0
local stale_seconds=0
local emu_health=""
start_ts=$(date +%s)
last_progress_ts=$start_ts
while kill -0 $GRADLE_PID 2>/dev/null; do
sleep "$HEARTBEAT_SECONDS"
now_ts=$(date +%s)
elapsed=$((now_ts - start_ts))
completed=$(find androidApp/build/outputs/androidTest-results/connected -name '*.xml' -type f -exec grep -ho "<testcase " {} + 2>/dev/null | wc -l | tr -d ' ')
if [[ -z "$completed" ]]; then
completed=0
fi
if [[ "$completed" -gt "$last_completed" ]]; then
last_progress_ts=$now_ts
last_completed=$completed
fi
if adb shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
emu_health="responsive"
else
emu_health="UNRESPONSIVE"
fi
stale_seconds=$((now_ts - last_progress_ts))
local elapsed_mm elapsed_ss
elapsed_mm=$((elapsed / 60))
elapsed_ss=$((elapsed % 60))
if [[ "$TOTAL_ANDROID_TESTS" -gt 0 ]]; then
echo "Android progress: ${completed}/${TOTAL_ANDROID_TESTS} tests complete | elapsed ${elapsed_mm}m${elapsed_ss}s | emulator ${emu_health}"
else
echo "Android progress: ${completed} tests complete | elapsed ${elapsed_mm}m${elapsed_ss}s | emulator ${emu_health}"
fi
if [[ "$elapsed" -ge "$HARD_TIMEOUT_SECONDS" ]]; then
echo "WATCHDOG: killing Gradle (PID $GRADLE_PID) after hard timeout ${HARD_TIMEOUT_SECONDS}s"
kill $GRADLE_PID 2>/dev/null || true
sleep 5
kill -9 $GRADLE_PID 2>/dev/null || true
break
fi
if [[ "$emu_health" == "UNRESPONSIVE" ]] && [[ "$stale_seconds" -ge "$UNRESPONSIVE_STALL_TIMEOUT_SECONDS" ]]; then
echo "WATCHDOG: killing Gradle (PID $GRADLE_PID) - emulator unresponsive and no progress for ${stale_seconds}s"
kill $GRADLE_PID 2>/dev/null || true
sleep 5
kill -9 $GRADLE_PID 2>/dev/null || true
break
fi
done
) &
local WATCHDOG_PID=$!
wait $GRADLE_PID 2>/dev/null || GRADLE_EXIT=$?
kill $WATCHDOG_PID 2>/dev/null || true
wait $WATCHDOG_PID 2>/dev/null || true
if [[ $GRADLE_EXIT -eq 137 ]] || [[ $GRADLE_EXIT -eq 143 ]]; then
echo -e "${RED}Android emulator tests terminated by watchdog${NC}"
ANDROID_FAIL=1
run_and_log "adb_restore_animations" adb shell "settings put global window_animation_scale 1; settings put global transition_animation_scale 1; settings put global animator_duration_scale 1" || true
return 1
elif [[ $GRADLE_EXIT -eq 0 ]]; then
echo -e "${GREEN}Android emulator tests PASSED${NC}"
ANDROID_PASS=1
# Emit runtime evidence for CI tracking
local android_duration_s=""
local android_test_count=""
if [[ -f androidApp/build/reports/androidTests/connected/debug/index.html ]]; then
android_test_count="$(grep -o '<div class="counter">[0-9]*</div>' androidApp/build/reports/androidTests/connected/debug/index.html | head -1 | grep -o '[0-9]*' || echo "")"
android_duration_s="$(grep -o '<div class="counter">[0-9a-z.]*s</div>' androidApp/build/reports/androidTests/connected/debug/index.html | head -1 | grep -o '[0-9.]*' || echo "")"
fi
echo "RUNTIME_EVIDENCE: {\"suite\": \"android_ui\", \"tests\": ${android_test_count:-0}, \"duration\": \"${android_duration_s:-unknown}s\", \"timestamp\": \"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}"
else
echo -e "${RED}Android emulator tests FAILED${NC}"
ANDROID_FAIL=1
echo "Test reports: androidApp/build/reports/androidTests/connected/debug/"
run_and_log "adb_restore_animations" adb shell "settings put global window_animation_scale 1; settings put global transition_animation_scale 1; settings put global animator_duration_scale 1" || true
return 1
fi
# Re-enable animations
run_and_log "adb_restore_animations" adb shell "settings put global window_animation_scale 1; settings put global transition_animation_scale 1; settings put global animator_duration_scale 1" || true
}
run_ios() {
echo -e "${YELLOW}=== iOS Simulator Tests ===${NC}"
# Find an available simulator
local SIM_ID
SIM_ID=$(xcrun simctl list devices available -j 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
for runtime, devices in data.get('devices', {}).items():
if 'iOS' in runtime:
for d in devices:
if d.get('isAvailable'):
print(d['udid'])
sys.exit(0)
sys.exit(1)
" 2>/dev/null) || true
if [[ -z "$SIM_ID" ]]; then
echo -e "${RED}ERROR: No available iOS simulator found.${NC}"
IOS_FAIL=1
return 1
fi
local SIM_NAME
SIM_NAME=$(xcrun simctl list devices available | grep "$SIM_ID" | sed 's/ (.*//' | xargs)
echo "Using simulator: $SIM_NAME ($SIM_ID)"
# Boot simulator if needed
xcrun simctl boot "$SIM_ID" 2>/dev/null || true
# Health check: verify simulator is actually responsive (not just "Booted" in simctl)
echo "Verifying simulator is responsive..."
local HEALTH_OK=false
for i in 1 2 3 4 5; do
if xcrun simctl spawn "$SIM_ID" launchctl print system >/dev/null 2>&1; then
HEALTH_OK=true
break
fi
echo " Attempt $i/5: simulator not responsive, waiting 5s..."
sleep 5
done
if [[ "$HEALTH_OK" != "true" ]]; then
echo -e "${RED}ERROR: Simulator $SIM_NAME ($SIM_ID) reports Booted but is not responsive.${NC}"
echo "Attempting full restart..."
xcrun simctl shutdown "$SIM_ID" 2>/dev/null || true
sleep 3
xcrun simctl boot "$SIM_ID" 2>/dev/null || true
sleep 10
if ! xcrun simctl spawn "$SIM_ID" launchctl print system >/dev/null 2>&1; then
echo -e "${RED}ERROR: Simulator still unresponsive after restart. Aborting.${NC}"
IOS_FAIL=1
return 1
fi
echo "Simulator recovered after restart."
fi
echo "Simulator is responsive."
# Generate Xcode project if needed
if [[ ! -f iosApp/iosApp.xcodeproj/project.pbxproj ]]; then
echo "Generating Xcode project..."
(cd iosApp && xcodegen generate)
fi
# --- Phase 1: Build (synchronous, fail-fast) ---
echo "Building iOS UI tests..."
local BUILD_DIR
BUILD_DIR=$(mktemp -d)
local BUILD_LOG
BUILD_LOG=$(mktemp)
local BUILD_START
BUILD_START=$(date +%s)
xcodebuild build-for-testing \
-project iosApp/iosApp.xcodeproj \
-scheme iosApp \
-destination "platform=iOS Simulator,id=$SIM_ID" \
-derivedDataPath "$BUILD_DIR" \
> "$BUILD_LOG" 2>&1
local BUILD_EXIT=$?
local BUILD_END
BUILD_END=$(date +%s)
echo "iOS build phase: $((BUILD_END - BUILD_START))s (exit=$BUILD_EXIT)"
if [[ $BUILD_EXIT -ne 0 ]]; then
echo -e "${RED}BUILD FAILED — last 30 lines:${NC}"
tail -30 "$BUILD_LOG"
rm -f "$BUILD_LOG"
rm -rf "$BUILD_DIR"
IOS_FAIL=1
return 1
fi
rm -f "$BUILD_LOG"
# Disable animations for stable, faster UI tests
echo "Disabling simulator animations..."
xcrun simctl spawn "$SIM_ID" defaults write com.apple.Accessibility ReduceMotionEnabled -bool YES 2>/dev/null || true
# Uninstall the app to ensure a clean database for tests
echo "Cleaning app data..."
xcrun simctl uninstall "$SIM_ID" com.periodvault.app 2>/dev/null || true
# --- Phase 2: Test (background with watchdog, parallel execution) ---
echo "Running iOS UI tests (parallel enabled)..."
local TEST_EXIT=0
local TEST_LOG
TEST_LOG=$(mktemp)
local RESULT_BUNDLE_DIR
RESULT_BUNDLE_DIR=$(mktemp -d)
local RESULT_BUNDLE_PATH="$RESULT_BUNDLE_DIR/ios-ui-tests.xcresult"
local TOTAL_IOS_TESTS=0
TOTAL_IOS_TESTS=$(find iosApp/iosAppUITests -name '*.swift' -print0 2>/dev/null | xargs -0 grep -hE '^[[:space:]]*func[[:space:]]+test' 2>/dev/null | wc -l | tr -d ' ')
if [[ -z "$TOTAL_IOS_TESTS" ]]; then
TOTAL_IOS_TESTS=0
fi
local TEST_START
TEST_START=$(date +%s)
xcodebuild test-without-building \
-project iosApp/iosApp.xcodeproj \
-scheme iosApp \
-destination "platform=iOS Simulator,id=$SIM_ID" \
-only-testing:iosAppUITests \
-derivedDataPath "$BUILD_DIR" \
-resultBundlePath "$RESULT_BUNDLE_PATH" \
-parallel-testing-enabled YES \
> "$TEST_LOG" 2>&1 &
local XCODE_PID=$!
# Progress/liveness watchdog:
# - emits heartbeat with completed test count and simulator health
# - fails fast when CoreSimulatorService is unhealthy
# - treats test completion, xcodebuild CPU, and log growth as activity
# - fails when startup/test activity stalls beyond configured thresholds
# - keeps a hard cap as a final safety net
local HEARTBEAT_SECONDS="${IOS_HEARTBEAT_SECONDS:-30}"
local STARTUP_PROGRESS_TIMEOUT_SECONDS="${IOS_STARTUP_PROGRESS_TIMEOUT_SECONDS:-900}"
local TEST_STALL_TIMEOUT_SECONDS="${IOS_TEST_STALL_TIMEOUT_SECONDS:-480}"
local UNRESPONSIVE_STALL_TIMEOUT_SECONDS="${IOS_UNRESPONSIVE_STALL_TIMEOUT_SECONDS:-120}"
local HARD_TIMEOUT_SECONDS="${IOS_HARD_TIMEOUT_SECONDS:-10800}" # 3 hours
local ACTIVE_CPU_THRESHOLD="${IOS_ACTIVE_CPU_THRESHOLD:-1.0}"
echo "iOS watchdog: heartbeat=${HEARTBEAT_SECONDS}s startup_timeout=${STARTUP_PROGRESS_TIMEOUT_SECONDS}s test_stall_timeout=${TEST_STALL_TIMEOUT_SECONDS}s unresponsive_timeout=${UNRESPONSIVE_STALL_TIMEOUT_SECONDS}s hard_timeout=${HARD_TIMEOUT_SECONDS}s cpu_active_threshold=${ACTIVE_CPU_THRESHOLD}%"
(
local start_ts now_ts elapsed
local last_test_progress_ts
local last_activity_ts
local completed=0
local last_completed=0
local stale_seconds=0
local sim_health=""
local first_test_seen=false
local simctl_health_output=""
local log_size=0
local last_log_size=0
local xcode_cpu="0.0"
local xcode_cpu_raw=""
start_ts=$(date +%s)
last_test_progress_ts=$start_ts
last_activity_ts=$start_ts
while kill -0 $XCODE_PID 2>/dev/null; do
sleep "$HEARTBEAT_SECONDS"
now_ts=$(date +%s)
elapsed=$((now_ts - start_ts))
# Keep watchdog alive before first completed test appears; do not fail on zero matches.
completed=$(grep -E -c "Test [Cc]ase .* (passed|failed)" "$TEST_LOG" 2>/dev/null || true)
if [[ -z "$completed" ]]; then
completed=0
fi
if [[ "$completed" -gt "$last_completed" ]]; then
last_test_progress_ts=$now_ts
last_activity_ts=$now_ts
last_completed=$completed
first_test_seen=true
fi
# xcodebuild output growth indicates ongoing work even when a test has not completed yet.
log_size=$(wc -c < "$TEST_LOG" 2>/dev/null || echo 0)
if [[ -n "$log_size" ]] && [[ "$log_size" -gt "$last_log_size" ]]; then
last_log_size=$log_size
last_activity_ts=$now_ts
fi
# CPU usage provides another liveness signal during long-running UI tests.
xcode_cpu_raw=$(ps -p "$XCODE_PID" -o %cpu= 2>/dev/null | tr -d ' ' || true)
if [[ -n "$xcode_cpu_raw" ]]; then
xcode_cpu="$xcode_cpu_raw"
else
xcode_cpu="0.0"
fi
if awk "BEGIN { exit !($xcode_cpu >= $ACTIVE_CPU_THRESHOLD) }"; then
last_activity_ts=$now_ts
fi
if simctl_health_output=$(xcrun simctl spawn "$SIM_ID" launchctl print system 2>&1); then
sim_health="responsive"
else
sim_health="UNRESPONSIVE"
# Fail fast when the simulator service itself is down. Waiting longer does not recover this state.
if echo "$simctl_health_output" | grep -Eiq "CoreSimulatorService connection became invalid|not connected to CoreSimulatorService|Unable to locate device set|Connection refused|simdiskimaged.*(crashed|not responding)|Unable to discover any Simulator runtimes"; then
echo "WATCHDOG: CoreSimulatorService unhealthy; killing xcodebuild (PID $XCODE_PID) immediately"
echo "$simctl_health_output" | head -5 | sed 's/^/ simctl: /'
kill $XCODE_PID 2>/dev/null || true
sleep 5
kill -9 $XCODE_PID 2>/dev/null || true
break
fi
fi
stale_seconds=$((now_ts - last_activity_ts))
local elapsed_mm elapsed_ss
elapsed_mm=$((elapsed / 60))
elapsed_ss=$((elapsed % 60))
if [[ "$TOTAL_IOS_TESTS" -gt 0 ]]; then
echo "iOS progress: ${completed}/${TOTAL_IOS_TESTS} tests complete | elapsed ${elapsed_mm}m${elapsed_ss}s | simulator ${sim_health} | xcodebuild cpu ${xcode_cpu}%"
else
echo "iOS progress: ${completed} tests complete | elapsed ${elapsed_mm}m${elapsed_ss}s | simulator ${sim_health} | xcodebuild cpu ${xcode_cpu}%"
fi
if [[ "$elapsed" -ge "$HARD_TIMEOUT_SECONDS" ]]; then
echo "WATCHDOG: killing xcodebuild (PID $XCODE_PID) after hard timeout ${HARD_TIMEOUT_SECONDS}s"
kill $XCODE_PID 2>/dev/null || true
sleep 5
kill -9 $XCODE_PID 2>/dev/null || true
break
fi
if [[ "$first_test_seen" != "true" ]] && [[ "$elapsed" -ge "$STARTUP_PROGRESS_TIMEOUT_SECONDS" ]]; then
echo "WATCHDOG: killing xcodebuild (PID $XCODE_PID) - no completed iOS test observed within startup timeout (${STARTUP_PROGRESS_TIMEOUT_SECONDS}s)"
kill $XCODE_PID 2>/dev/null || true
sleep 5
kill -9 $XCODE_PID 2>/dev/null || true
break
fi
if [[ "$first_test_seen" == "true" ]] && [[ "$stale_seconds" -ge "$TEST_STALL_TIMEOUT_SECONDS" ]]; then
echo "WATCHDOG: killing xcodebuild (PID $XCODE_PID) - no iOS test activity for ${stale_seconds}s"
kill $XCODE_PID 2>/dev/null || true
sleep 5
kill -9 $XCODE_PID 2>/dev/null || true
break
fi
if [[ "$sim_health" == "UNRESPONSIVE" ]] && [[ "$stale_seconds" -ge "$UNRESPONSIVE_STALL_TIMEOUT_SECONDS" ]]; then
echo "WATCHDOG: killing xcodebuild (PID $XCODE_PID) - simulator unresponsive and no test activity for ${stale_seconds}s"
kill $XCODE_PID 2>/dev/null || true
sleep 5
kill -9 $XCODE_PID 2>/dev/null || true
break
fi
done
) &
local WATCHDOG_PID=$!
wait $XCODE_PID 2>/dev/null || TEST_EXIT=$?
kill $WATCHDOG_PID 2>/dev/null || true
wait $WATCHDOG_PID 2>/dev/null || true
local TEST_END
TEST_END=$(date +%s)
echo "iOS test phase: $((TEST_END - TEST_START))s (exit=$TEST_EXIT)"
echo "iOS total (build+test): $((TEST_END - BUILD_START))s"
# Show test summary (passed/failed counts and any failures)
echo "--- Test Results ---"
grep -E "Test [Cc]ase .* (passed|failed)" "$TEST_LOG" || true
echo ""
echo "--- Failures ---"
grep -E "(FAIL|error:|\*\* TEST FAILED)" "$TEST_LOG" || echo " (none)"
echo ""
echo "--- Last 20 lines ---"
tail -20 "$TEST_LOG"
rm -f "$TEST_LOG"
if [[ $TEST_EXIT -eq 0 ]]; then
local SKIP_ALLOWLIST="${IOS_SKIPPED_TESTS_ALLOWLIST:-audit/ios-skipped-tests-allowlist.txt}"
if ! bash "$SCRIPT_DIR/validate-ios-skipped-tests.sh" "$RESULT_BUNDLE_PATH" "$SKIP_ALLOWLIST"; then
echo -e "${RED}iOS skipped-test gate FAILED${NC}"
TEST_EXIT=1
fi
fi
rm -rf "$RESULT_BUNDLE_DIR"
rm -rf "$BUILD_DIR"
# Re-enable animations
xcrun simctl spawn "$SIM_ID" defaults write com.apple.Accessibility ReduceMotionEnabled -bool NO 2>/dev/null || true
if [[ $TEST_EXIT -eq 137 ]] || [[ $TEST_EXIT -eq 143 ]]; then
echo -e "${RED}iOS simulator tests terminated by watchdog${NC}"
IOS_FAIL=1
return 1
elif [[ $TEST_EXIT -eq 0 ]]; then
echo -e "${GREEN}iOS simulator tests PASSED${NC}"
IOS_PASS=1
# Emit runtime evidence for CI tracking
local ios_test_count=""
ios_test_count="$TOTAL_IOS_TESTS"
local ios_elapsed_s=""
ios_elapsed_s="$(($(date +%s) - $(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +%s 2>/dev/null || echo 0)))"
echo "RUNTIME_EVIDENCE: {\"suite\": \"ios_ui\", \"tests\": ${ios_test_count:-0}, \"timestamp\": \"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}"
else
echo -e "${RED}iOS simulator tests FAILED${NC}"
IOS_FAIL=1
return 1
fi
}
echo "================================================"
echo " PeriodVault Emulator/Simulator Test Runner"
echo "================================================"
echo ""
case "$PLATFORM" in
android)
run_android
;;
ios)
run_ios
;;
all)
run_android || true
echo ""
run_ios || true
;;
*)
echo "Usage: $0 [android|ios|all]"
exit 1
;;
esac
echo ""
echo "================================================"
echo " Results Summary"
echo "================================================"
if [[ "$PLATFORM" == "all" || "$PLATFORM" == "android" ]]; then
if [[ $ANDROID_PASS -eq 1 ]]; then
echo -e " Android: ${GREEN}PASSED${NC}"
elif [[ $ANDROID_FAIL -eq 1 ]]; then
echo -e " Android: ${RED}FAILED${NC}"
else
echo -e " Android: ${YELLOW}SKIPPED${NC}"
fi
fi
if [[ "$PLATFORM" == "all" || "$PLATFORM" == "ios" ]]; then
if [[ $IOS_PASS -eq 1 ]]; then
echo -e " iOS: ${GREEN}PASSED${NC}"
elif [[ $IOS_FAIL -eq 1 ]]; then
echo -e " iOS: ${RED}FAILED${NC}"
else
echo -e " iOS: ${YELLOW}SKIPPED${NC}"
fi
fi
echo "================================================"
# Exit with failure if any platform failed
if [[ $ANDROID_FAIL -eq 1 ]] || [[ $IOS_FAIL -eq 1 ]]; then
exit 1
fi