Centralize all notification alarm scheduling through NotifyReceiver.scheduleExactNotification() with idempotence checks to prevent duplicate alarms. Implement one-alarm policy using setAlarmClock() only. Fix test harness alarm counting to deduplicate by Alarm handle. Plugin Changes: - Add ScheduleSource enum to track scheduling paths (INITIAL_SETUP, ROLLOVER_ON_FIRE, etc.) - Add DB-level idempotence check before scheduling (prevents logical duplicates) - Add explicit alarm cancellation before scheduling (safety net) - Implement one-alarm policy: use setAlarmClock() only, no setExact* fallbacks for same event - Add deep logging for all AlarmManager calls (variant, requestCode, pendingIntentHash) - Update all rollover paths (DailyNotificationReceiver, DailyNotificationWorker) to use centralized function with ROLLOVER_ON_FIRE source - Add @JvmStatic annotation to scheduleExactNotification for Java interop Test Harness Changes: - Fix get_plugin_alarm_count() to deduplicate by Alarm handle (prevents double-counting same alarm in main list and "Next wake from idle" section) - Update TEST 0 messaging: treat 0 alarms as race condition (inconclusive, not failure) - Make post-rollover check the authoritative assertion point (only fails on >1 or 0 alarms) - Remove redundant "Found 0 alarms - test may not be accurate" messages This fixes the duplicate alarm bug where two distinct AlarmManager entries were created for the same daily notification, violating the "one notification per day" contract.
347 lines
11 KiB
Bash
Executable File
347 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
set -euo pipefail
|
||
|
||
# ========================================
|
||
# Phase 2 Testing Script – Force Stop Recovery
|
||
# ========================================
|
||
|
||
# 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)
|
||
FORCE_STOP_SCENARIO_VALUE="FORCE_STOP"
|
||
COLD_START_SCENARIO_VALUE="COLD_START"
|
||
NONE_SCENARIO_VALUE="NONE"
|
||
BOOT_SCENARIO_VALUE="BOOT"
|
||
|
||
# Allow selecting specific tests on the command line (e.g. ./test-phase2.sh 2 3)
|
||
SELECTED_TESTS=()
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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 system_count
|
||
before_count="$(get_plugin_alarm_count)"
|
||
system_count="$(get_system_alarm_count)"
|
||
info "Plugin alarms before force stop: $before_count (expected: 1)"
|
||
info "System/other alarms: $system_count (for context)"
|
||
|
||
if [[ "$before_count" -eq 0 ]]; then
|
||
warn "No plugin alarms found before force stop; TEST 1 may not be meaningful."
|
||
elif [[ "$before_count" -eq 1 ]]; then
|
||
ok "Single plugin alarm confirmed (one per day)"
|
||
else
|
||
warn "Found $before_count plugin alarms (expected: 1)"
|
||
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 system_after
|
||
after_count="$(get_plugin_alarm_count)"
|
||
system_after="$(get_system_alarm_count)"
|
||
info "Plugin alarms after force stop: $after_count (expected: 0)"
|
||
info "System/other alarms: $system_after (for context)"
|
||
show_alarms
|
||
|
||
if [[ "$after_count" -gt 0 ]]; then
|
||
warn "Plugin 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 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")"
|
||
|
||
echo
|
||
info "Parsed recovery summary:"
|
||
echo " scenario = ${scenario:-<none>}"
|
||
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: Verify that heavy FORCE_STOP recovery does not run when alarms are still present."
|
||
|
||
pause
|
||
|
||
substep "Step 1: Launch app & schedule notifications"
|
||
launch_app
|
||
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
|
||
local before system_before
|
||
before="$(get_plugin_alarm_count)"
|
||
system_before="$(get_system_alarm_count)"
|
||
info "Plugin alarms before stop: $before (expected: 1)"
|
||
info "System/other alarms: $system_before (for context)"
|
||
|
||
if [[ "$before" -eq 0 ]]; then
|
||
warn "No plugin alarms found; TEST 2 may not be meaningful."
|
||
elif [[ "$before" -eq 1 ]]; then
|
||
ok "Single plugin alarm confirmed (one per day)"
|
||
else
|
||
warn "Found $before plugin alarms (expected: 1)"
|
||
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 system_after
|
||
after="$(get_plugin_alarm_count)"
|
||
system_after="$(get_system_alarm_count)"
|
||
info "Plugin alarms after soft stop: $after (expected: 1)"
|
||
info "System/other alarms: $system_after (for context)"
|
||
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: Relaunch app and check recovery logs"
|
||
clear_logs
|
||
launch_app
|
||
sleep 5
|
||
|
||
info "Collecting recovery logs..."
|
||
local logs
|
||
logs="$(get_recovery_logs)"
|
||
echo "$logs"
|
||
|
||
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")"
|
||
|
||
echo
|
||
info "Parsed recovery summary:"
|
||
echo " scenario = ${scenario:-<none>}"
|
||
echo " rescheduled= ${rescheduled}"
|
||
echo " missed = ${missed}"
|
||
echo " verified = ${verified}"
|
||
echo " errors = ${errors}"
|
||
echo
|
||
|
||
if [[ "$errors" -gt 0 ]]; then
|
||
error "Recovery reported errors>0 (errors=$errors)"
|
||
fi
|
||
|
||
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
|
||
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
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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 for the first time"
|
||
launch_app
|
||
sleep 5
|
||
|
||
substep "Step 4: Collect logs and ensure no force-stop recovery ran"
|
||
local logs
|
||
logs="$(get_recovery_logs)"
|
||
echo "$logs"
|
||
|
||
local scenario rescheduled
|
||
scenario="$(extract_scenario_from_logs "$logs")"
|
||
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||
|
||
echo
|
||
info "Parsed summary:"
|
||
echo " scenario = ${scenario:-<none>}"
|
||
echo " rescheduled= ${rescheduled}"
|
||
echo
|
||
|
||
if [[ -z "$logs" ]]; then
|
||
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
|
||
info "TEST 3: Logs present but no rescheduling; review scenario handling to ensure it's explicit about NONE / FIRST_LAUNCH."
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Main
|
||
# ------------------------------------------------------------------------------
|
||
|
||
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 Phase 2 tests."
|
||
echo "You'll be prompted when UI interaction is needed."
|
||
echo
|
||
|
||
pause
|
||
|
||
require_adb_device
|
||
build_app
|
||
install_app
|
||
|
||
if should_run_test "1" SELECTED_TESTS; then
|
||
test1_force_stop_cleared_alarms
|
||
pause
|
||
fi
|
||
|
||
if should_run_test "2" SELECTED_TESTS; then
|
||
test2_force_stop_intact_alarms
|
||
pause
|
||
fi
|
||
|
||
if should_run_test "3" SELECTED_TESTS; then
|
||
test3_first_launch_no_schedules
|
||
fi
|
||
|
||
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 " - Verify FORCE_STOP scenario is not incorrectly triggered when alarms are still present"
|
||
echo
|
||
echo "TEST 3: First Launch / No Schedules"
|
||
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!"
|
||
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 "$@"
|