Files
daily-notification-plugin/test-apps/android-test-app/test-phase2.sh
Matthew Raymer c8a3906449 docs(test): add Phase 2 force stop recovery testing infrastructure
Adds documentation and test harness for Phase 2 (Force Stop Detection & Recovery).

Changes:
- Add PHASE2-EMULATOR-TESTING.md with detailed test procedures
- Add PHASE2-VERIFICATION.md with test matrix and verification template
- Add test-phase2.sh automated test harness

Test harness features:
- 3 test cases: force stop with cleared alarms, intact alarms, empty DB
- Automatic force stop simulation via adb
- Log parsing for scenario detection and recovery results
- UI prompts for plugin configuration and scheduling

Related:
- Directive: android-implementation-directive-phase2.md
- Requirements: docs/alarms/03-plugin-requirements.md §3.1.4
- Testing: docs/alarms/PHASE2-EMULATOR-TESTING.md
- Verification: docs/alarms/PHASE2-VERIFICATION.md
2025-11-27 10:01:40 +00:00

531 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# ========================================
# Phase 2 Testing Script Force Stop Recovery
# ========================================
# --- Config -------------------------------------------------------------------
APP_ID="com.timesafari.dailynotification"
APK_PATH="./app/build/outputs/apk/debug/app-debug.apk"
ADB_BIN="${ADB_BIN:-adb}"
# Log tags / patterns (matched to actual ReactivationManager logs)
REACTIVATION_TAG="DNP-REACTIVATION"
SCENARIO_KEY="Detected scenario: "
FORCE_STOP_SCENARIO_VALUE="FORCE_STOP"
COLD_START_SCENARIO_VALUE="COLD_START"
NONE_SCENARIO_VALUE="NONE"
BOOT_SCENARIO_VALUE="BOOT"
# ------------------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------------------
section() {
echo
echo "========================================"
echo "$1"
echo "========================================"
echo
}
substep() {
echo "$1"
}
info() {
echo -e " $1"
}
ok() {
echo -e "$1"
}
warn() {
echo -e "⚠️ $1"
}
error() {
echo -e "$1"
}
pause() {
echo
read -rp "Press Enter when ready to continue..."
echo
}
ui_prompt() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "👆 UI ACTION REQUIRED"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "$1"
echo
read -rp "Press Enter after completing the action above..."
echo
}
run_cmd() {
# Wrapper so output is visible but failures stop the script
# Usage: run_cmd "description" cmd...
local desc="$1"; shift
substep "$desc"
echo " $*"
if "$@"; then
ok "$desc"
else
error "$desc failed"
exit 1
fi
}
require_adb_device() {
section "Pre-Flight Checks"
if ! $ADB_BIN devices | awk 'NR>1 && $2=="device"{found=1} END{exit !found}'; then
error "No emulator/device in 'device' state. Start your emulator first."
exit 1
fi
ok "ADB device connected"
info "Checking emulator status..."
if ! $ADB_BIN shell getprop sys.boot_completed | grep -q "1"; then
info "Waiting for emulator to boot..."
$ADB_BIN wait-for-device
while [ "$($ADB_BIN shell getprop sys.boot_completed)" != "1" ]; do
sleep 2
done
fi
ok "Emulator is ready"
}
build_app() {
section "Building Test App"
substep "Step 1: Building debug APK..."
if ./gradlew :app:assembleDebug; then
ok "Build successful"
else
error "Build failed"
exit 1
fi
if [[ -f "$APK_PATH" ]]; then
ok "APK ready: $APK_PATH"
else
error "APK not found at $APK_PATH"
exit 1
fi
}
install_app() {
section "Installing App"
substep "Step 1: Uninstalling existing app (if present)..."
set +e
uninstall_output="$($ADB_BIN uninstall "$APP_ID" 2>&1)"
uninstall_status=$?
set -e
if [[ $uninstall_status -ne 0 ]]; then
if grep -q "DELETE_FAILED_INTERNAL_ERROR" <<<"$uninstall_output"; then
info "No existing app to uninstall (continuing)"
else
warn "Uninstall returned non-zero status: $uninstall_output (continuing anyway)"
fi
else
ok "Previous app uninstall succeeded"
fi
substep "Step 2: Installing new APK..."
if $ADB_BIN install -r "$APK_PATH"; then
ok "App installed successfully"
else
error "App installation failed"
exit 1
fi
substep "Step 3: Verifying installation..."
if $ADB_BIN shell pm list packages | grep -q "$APP_ID"; then
ok "App verified in package list"
else
error "App not found in package list"
exit 1
fi
info "Clearing logcat buffer..."
$ADB_BIN logcat -c
ok "Logs cleared"
}
launch_app() {
info "Launching app..."
$ADB_BIN shell am start -n "${APP_ID}/.MainActivity" >/dev/null 2>&1
sleep 3 # Give app time to load
ok "App launched"
}
clear_logs() {
info "Clearing logcat buffer..."
$ADB_BIN logcat -c
ok "Logs cleared"
}
show_alarms() {
info "Checking AlarmManager status..."
echo
$ADB_BIN shell dumpsys alarm | grep -A3 "$APP_ID" || true
echo
}
count_alarms() {
# Returns count of alarms for our app
$ADB_BIN shell dumpsys alarm | grep -c "$APP_ID" || echo "0"
}
force_stop_app() {
info "Force-stopping app..."
$ADB_BIN shell am force-stop "$APP_ID"
sleep 2
ok "Force stop requested"
}
get_recovery_logs() {
# Collect recent reactivation logs
$ADB_BIN logcat -d | grep "$REACTIVATION_TAG" || true
}
extract_field_from_logs() {
# Usage: extract_field_from_logs "<logs>" "<field_name>"
local logs="$1"
local field="$2"
# Looks for patterns like "field=NUMBER" and returns NUMBER (or 0)
local value
value="$(grep -oE "${field}=[0-9]+" <<<"$logs" | tail -n1 | sed "s/${field}=//" || true)"
if [[ -z "$value" ]]; then
echo "0"
else
echo "$value"
fi
}
extract_scenario_from_logs() {
local logs="$1"
local scen
# Looks for "Detected scenario: FORCE_STOP" format
scen="$(grep -oE "${SCENARIO_KEY}[A-Z_]+" <<<"$logs" | tail -n1 | sed "s/${SCENARIO_KEY}//" || true)"
echo "$scen"
}
# ------------------------------------------------------------------------------
# TEST 1 Force Stop with Cleared Alarms
# ------------------------------------------------------------------------------
test1_force_stop_cleared_alarms() {
section "TEST 1: Force Stop Alarms Cleared"
echo "Purpose: Verify force stop detection and alarm rescheduling when alarms are cleared."
pause
substep "Step 1: Launch app & check plugin status"
launch_app
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf either shows ❌ or 'Not configured', click 'Configure Plugin', wait until both are ✅, then press Enter."
ui_prompt "Now schedule at least one future notification (e.g., click 'Test Notification' to schedule for a few minutes in the future)."
substep "Step 2: Verify alarms are scheduled"
show_alarms
local before_count
before_count="$(count_alarms)"
info "Alarm count before force stop: $before_count"
if [[ "$before_count" -eq 0 ]]; then
warn "No alarms found before force stop; TEST 1 may not be meaningful."
fi
pause
substep "Step 3: Force stop app (should clear alarms on many devices)"
force_stop_app
substep "Step 4: Check alarms after force stop"
local after_count
after_count="$(count_alarms)"
info "Alarm count after force stop: $after_count"
show_alarms
if [[ "$after_count" -gt 0 ]]; then
warn "Alarms still present after force stop. This device/OS may not clear alarms on force stop."
warn "TEST 1 will continue but may not fully validate FORCE_STOP scenario."
fi
pause
substep "Step 5: Launch app (triggers recovery) and capture logs"
clear_logs
launch_app
sleep 5 # give recovery a moment to run
info "Collecting recovery logs..."
local logs
logs="$(get_recovery_logs)"
echo "$logs"
local missed rescheduled verified errors scenario
missed="$(extract_field_from_logs "$logs" "missed")"
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
verified="$(extract_field_from_logs "$logs" "verified")"
errors="$(extract_field_from_logs "$logs" "errors")"
scenario="$(extract_scenario_from_logs "$logs")"
echo
info "Parsed recovery summary:"
echo " scenario = ${scenario:-<none>}"
echo " missed = ${missed}"
echo " rescheduled= ${rescheduled}"
echo " verified = ${verified}"
echo " errors = ${errors}"
echo
if [[ "$errors" -gt 0 ]]; then
error "Recovery reported errors>0 (errors=$errors)"
fi
if [[ "$scenario" == "$FORCE_STOP_SCENARIO_VALUE" && "$rescheduled" -gt 0 ]]; then
ok "TEST 1 PASSED: Force stop detected and alarms rescheduled (scenario=$scenario, rescheduled=$rescheduled)."
elif [[ "$scenario" == "$FORCE_STOP_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
warn "TEST 1: scenario=FORCE_STOP but rescheduled=0. Check implementation or logs."
elif [[ "$after_count" -gt 0 ]]; then
info "TEST 1: Device/emulator kept alarms after force stop; FORCE_STOP scenario may not trigger here."
if [[ "$rescheduled" -gt 0 ]]; then
info "Recovery still worked (rescheduled=$rescheduled), but scenario was ${scenario:-COLD_START} instead of FORCE_STOP"
fi
else
warn "TEST 1: Expected FORCE_STOP scenario not clearly detected. Review logs and scenario detection logic."
info "Scenario detected: ${scenario:-<none>}, rescheduled=$rescheduled"
fi
substep "Step 6: Verify alarms are rescheduled in AlarmManager"
show_alarms
}
# ------------------------------------------------------------------------------
# TEST 2 Force Stop / Process Stop with Intact Alarms
# ------------------------------------------------------------------------------
test2_force_stop_intact_alarms() {
section "TEST 2: Force Stop / Process Stop Alarms Intact"
echo "Purpose: Ensure we do NOT run heavy force-stop recovery when alarms are intact."
pause
substep "Step 1: Launch app & ensure plugin configured"
launch_app
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf needed, click 'Configure Plugin', then press Enter."
ui_prompt "Click 'Test Notification' to schedule another notification a few minutes in the future."
substep "Step 2: Verify alarms are scheduled"
show_alarms
local before
before="$(count_alarms)"
info "Alarm count before stop: $before"
if [[ "$before" -eq 0 ]]; then
warn "No alarms found; TEST 2 may not be meaningful."
fi
pause
substep "Step 3: Simulate a 'soft' stop or process kill that does NOT clear alarms"
info "Killing app process (non-destructive - may not clear alarms)..."
$ADB_BIN shell am kill "$APP_ID" || true
sleep 2
ok "Kill signal sent (soft stop)"
substep "Step 4: Verify alarms are still scheduled"
local after
after="$(count_alarms)"
info "Alarm count after soft stop: $after"
show_alarms
if [[ "$after" -eq 0 ]]; then
warn "Alarms appear cleared after soft stop; this environment may not distinguish TEST 2 well."
fi
pause
substep "Step 5: Launch app (triggers recovery) and capture logs"
clear_logs
launch_app
sleep 5
info "Collecting recovery logs..."
local logs
logs="$(get_recovery_logs)"
echo "$logs"
local missed rescheduled verified errors scenario
missed="$(extract_field_from_logs "$logs" "missed")"
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
verified="$(extract_field_from_logs "$logs" "verified")"
errors="$(extract_field_from_logs "$logs" "errors")"
scenario="$(extract_scenario_from_logs "$logs")"
echo
info "Parsed recovery summary:"
echo " scenario = ${scenario:-<none>}"
echo " missed = ${missed}"
echo " rescheduled= ${rescheduled}"
echo " verified = ${verified}"
echo " errors = ${errors}"
echo
if [[ "$errors" -gt 0 ]]; then
error "Recovery reported errors>0 (errors=$errors)"
fi
if [[ "$scenario" != "$FORCE_STOP_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
ok "TEST 2 PASSED: No heavy force-stop recovery when alarms intact (scenario=$scenario, rescheduled=$rescheduled)."
elif [[ "$scenario" == "$FORCE_STOP_SCENARIO_VALUE" ]]; then
warn "TEST 2: scenario=FORCE_STOP detected but alarms were intact. Check scenario detection logic."
else
info "TEST 2: Some rescheduling occurred (rescheduled=$rescheduled). This might be OK if alarms were actually missing."
fi
}
# ------------------------------------------------------------------------------
# TEST 3 First Launch / Empty DB Safeguard
# ------------------------------------------------------------------------------
test3_first_launch_no_schedules() {
section "TEST 3: First Launch / No Schedules Safeguard"
echo "Purpose: Ensure force-stop recovery is NOT triggered when DB is empty or plugin isn't configured."
pause
substep "Step 1: Uninstall app to clear DB/state"
set +e
$ADB_BIN uninstall "$APP_ID" >/dev/null 2>&1
set -e
ok "App uninstalled (state cleared)"
substep "Step 2: Reinstall app"
if $ADB_BIN install -r "$APK_PATH"; then
ok "App installed"
else
error "Reinstall failed"
exit 1
fi
info "Clearing logcat..."
$ADB_BIN logcat -c
ok "Logs cleared"
pause
substep "Step 3: Launch app WITHOUT scheduling anything"
launch_app
sleep 5
info "Collecting recovery logs..."
local logs
logs="$($ADB_BIN logcat -d | grep "$REACTIVATION_TAG" || true)"
echo "$logs"
local scenario rescheduled missed
scenario="$(extract_scenario_from_logs "$logs")"
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
missed="$(extract_field_from_logs "$logs" "missed")"
echo
info "Parsed recovery summary:"
echo " scenario = ${scenario:-<none>}"
echo " rescheduled= ${rescheduled}"
echo " missed = ${missed}"
echo
# Check for explicit "No schedules" or "skipping recovery" messages
if echo "$logs" | grep -qiE "No schedules|skipping recovery|first launch"; then
ok "TEST 3 PASSED: No recovery logs or explicit skip message when there are no schedules (safe behavior)."
return
fi
if [[ -z "$logs" ]]; then
ok "TEST 3 PASSED: No recovery logs when there are no schedules (safe behavior)."
return
fi
# If you explicitly log a NONE scenario, check for that:
if [[ "$scenario" == "$NONE_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
ok "TEST 3 PASSED: NONE scenario detected with no rescheduling."
elif [[ "$scenario" == "$FORCE_STOP_SCENARIO_VALUE" ]]; then
error "TEST 3 FAILED: FORCE_STOP scenario detected on first launch / empty DB. This should not happen."
elif [[ "$rescheduled" -gt 0 ]]; then
warn "TEST 3: rescheduled>0 on first launch / empty DB. Check that force-stop recovery isn't misfiring."
else
info "TEST 3: Logs present but no rescheduling; review scenario handling to ensure it's explicit about NONE / FIRST_LAUNCH."
fi
}
# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------
main() {
echo
echo "========================================"
echo "Phase 2 Testing Script Force Stop Recovery"
echo "========================================"
echo
echo "This script will guide you through all Phase 2 tests."
echo "You'll be prompted when UI interaction is needed."
echo
pause
require_adb_device
build_app
install_app
test1_force_stop_cleared_alarms
pause
test2_force_stop_intact_alarms
pause
test3_first_launch_no_schedules
section "Testing Complete"
echo "Test Results Summary (see logs above for details):"
echo
echo "TEST 1: Force Stop Alarms Cleared"
echo " - Check logs for scenario=$FORCE_STOP_SCENARIO_VALUE and rescheduled>0"
echo
echo "TEST 2: Force Stop / Process Stop Alarms Intact"
echo " - Check that heavy FORCE_STOP recovery did NOT run when alarms are still present"
echo
echo "TEST 3: First Launch / No Schedules"
echo " - Check that no force-stop recovery runs, or that NONE scenario is logged with rescheduled=0"
echo
ok "Phase 2 testing script complete!"
echo
echo "Next steps:"
echo " - Review logs above"
echo " - Capture snippets into PHASE2-EMULATOR-TESTING.md"
echo " - Update PHASE2-VERIFICATION.md and unified directive status matrix"
echo
}
main "$@"