refactor(test): extract shared helpers into alarm-test-lib.sh

Extract common helper functions from test-phase1.sh, test-phase2.sh,
and test-phase3.sh into a shared library (alarm-test-lib.sh) to reduce
code duplication and improve maintainability.

Changes:
- Create alarm-test-lib.sh with shared configuration, UI helpers, ADB
  helpers, log parsing, and test selection logic
- Refactor all three phase test scripts to source the shared library
- Remove ~200 lines of duplicated code across the three scripts
- Preserve all existing behavior, CLI arguments, and test semantics
- Maintain Phase 1 compatibility (print_* functions, VERIFY_FIRE flag)
- Update all adb references to use $ADB_BIN variable
- Standardize alarm counting to use shared count_alarms() function

Benefits:
- Single source of truth for shared helpers
- Easier maintenance (fix once, benefits all scripts)
- Consistent behavior across all test phases
- No functional changes to test execution or results
This commit is contained in:
Matthew Raymer
2025-11-28 08:53:42 +00:00
parent 73301f7d1d
commit 07ace32982
4 changed files with 813 additions and 728 deletions

View File

@@ -6,219 +6,19 @@ 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}"
# Source shared library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/alarm-test-lib.sh"
# Phase 2 specific configuration
# 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"
}
# Allow selecting specific tests on the command line (e.g. ./test-phase2.sh 2 3)
SELECTED_TESTS=()
# ------------------------------------------------------------------------------
# TEST 1 Force Stop with Cleared Alarms
@@ -276,17 +76,15 @@ test1_force_stop_cleared_alarms() {
logs="$(get_recovery_logs)"
echo "$logs"
local missed rescheduled verified errors scenario
missed="$(extract_field_from_logs "$logs" "missed")"
local scenario rescheduled verified errors
scenario="$(extract_scenario_from_logs "$logs")"
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}"
@@ -321,16 +119,13 @@ test1_force_stop_cleared_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."
echo "Purpose: Verify that heavy FORCE_STOP recovery does not run when alarms are still present."
pause
substep "Step 1: Launch app & ensure plugin configured"
substep "Step 1: Launch app & schedule notifications"
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."
ui_prompt "In the app UI, ensure plugin is configured and schedule at least one future notification.\n\nPress Enter when done."
substep "Step 2: Verify alarms are scheduled"
show_alarms
@@ -362,7 +157,7 @@ test2_force_stop_intact_alarms() {
pause
substep "Step 5: Launch app (triggers recovery) and capture logs"
substep "Step 5: Relaunch app and check recovery logs"
clear_logs
launch_app
sleep 5
@@ -372,18 +167,18 @@ test2_force_stop_intact_alarms() {
logs="$(get_recovery_logs)"
echo "$logs"
local missed rescheduled verified errors scenario
missed="$(extract_field_from_logs "$logs" "missed")"
local scenario rescheduled missed verified errors
scenario="$(extract_scenario_from_logs "$logs")"
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
missed="$(extract_field_from_logs "$logs" "missed")"
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 " missed = ${missed}"
echo " verified = ${verified}"
echo " errors = ${errors}"
echo
@@ -392,12 +187,11 @@ test2_force_stop_intact_alarms() {
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."
if [[ "$after" -gt 0 && "$rescheduled" -eq 0 && "$scenario" != "$FORCE_STOP_SCENARIO_VALUE" ]]; then
ok "TEST 2 PASSED: Alarms remained intact, and FORCE_STOP scenario did not run (scenario=$scenario, rescheduled=0)."
else
info "TEST 2: Some rescheduling occurred (rescheduled=$rescheduled). This might be OK if alarms were actually missing."
warn "TEST 2: Verify that FORCE_STOP recovery didn't misfire when alarms were intact."
info "Scenario=${scenario:-<none>}, rescheduled=$rescheduled, after_count=$after"
fi
}
@@ -432,43 +226,29 @@ test3_first_launch_no_schedules() {
pause
substep "Step 3: Launch app WITHOUT scheduling anything"
substep "Step 3: Launch app for the first time"
launch_app
sleep 5
info "Collecting recovery logs..."
substep "Step 4: Collect logs and ensure no force-stop recovery ran"
local logs
logs="$($ADB_BIN logcat -d | grep "$REACTIVATION_TAG" || true)"
logs="$(get_recovery_logs)"
echo "$logs"
local scenario rescheduled missed
local scenario rescheduled
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:"
info "Parsed 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."
ok "TEST 3 PASSED: No force-stop recovery logs on first launch."
elif [[ "$scenario" == "$NONE_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
ok "TEST 3 PASSED: NONE scenario logged with rescheduled=0 on first launch."
elif [[ "$rescheduled" -gt 0 ]]; then
warn "TEST 3: rescheduled>0 on first launch / empty DB. Check that force-stop recovery isn't misfiring."
else
@@ -481,12 +261,26 @@ test3_first_launch_no_schedules() {
# ------------------------------------------------------------------------------
main() {
# Allow selecting specific tests: e.g. `./test-phase2.sh 1 3`
if [[ "$#" -gt 0 && ( "$1" == "-h" || "$1" == "--help" ) ]]; then
echo "Usage: $0 [TEST_IDS...]"
echo
echo "If no TEST_IDS are given, all tests (1, 2, 3) will run."
echo "Examples:"
echo " $0 # run all tests"
echo " $0 1 # run only TEST 1"
echo " $0 2 3 # run only TEST 2 and TEST 3"
return 0
fi
SELECTED_TESTS=("$@")
echo
echo "========================================"
echo "Phase 2 Testing Script Force Stop Recovery"
echo "========================================"
echo
echo "This script will guide you through all Phase 2 tests."
echo "This script will guide you through Phase 2 tests."
echo "You'll be prompted when UI interaction is needed."
echo
@@ -496,13 +290,19 @@ main() {
build_app
install_app
test1_force_stop_cleared_alarms
pause
if should_run_test "1" SELECTED_TESTS; then
test1_force_stop_cleared_alarms
pause
fi
test2_force_stop_intact_alarms
pause
if should_run_test "2" SELECTED_TESTS; then
test2_force_stop_intact_alarms
pause
fi
test3_first_launch_no_schedules
if should_run_test "3" SELECTED_TESTS; then
test3_first_launch_no_schedules
fi
section "Testing Complete"
@@ -512,10 +312,10 @@ main() {
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 " - Verify FORCE_STOP scenario is not incorrectly triggered 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 " - Confirm that no force-stop recovery runs, or that NONE/FIRST_LAUNCH scenario is logged with rescheduled=0"
echo
ok "Phase 2 testing script complete!"