Implement P0-P5 directives for operator clarity, consistent outcomes, and easy evidence capture across all test phases. Changes: - alarm-test-lib.sh: Add evidence collection (capture_alarms, capture_logcat, capture_screenshot), verdict functions (verdict_pass/warn/fail), run directory management, and release gating support (RELEASE_GATE_PHASE3) - test-phase1.sh: Refactor to unified framework with CLI modes (--setup, --run, --smoke, --all, --ci), micro-prompts, evidence capture, and verdict blocks for all 5 tests - test-phase2.sh: Add evidence capture, verdict blocks, and STRICTNESS policy (soft/hard) for warn vs fail behavior - test-phase3.sh: Add evidence capture, verdict blocks, release gating (--gate-phase3), and fatigue reduction (time estimates, automation hints) - RUNBOOK-TESTING.md: New comprehensive operator guide (669 lines) covering prerequisites, all phases, evidence locations, verdict interpretation, common failures, and troubleshooting All test scripts now use consistent UI helpers (section, substep, info, ok, warn, error), standardized evidence collection, and clear verdict reporting. Evidence is saved to timestamped run directories (runs/<RUN_ID>/) with alarms, logs, and screenshots organized by test phase and scenario. Tests pass with consistent presentation and reproducible evidence collection.
1295 lines
47 KiB
Bash
Executable File
1295 lines
47 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Phase 1 Testing Script - Interactive Test Runner
|
|
# Guides through all Phase 1 tests with clear prompts for UI interaction
|
|
|
|
# Strict mode (P0.2)
|
|
set -euo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
# Source shared library
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "${SCRIPT_DIR}/alarm-test-lib.sh"
|
|
|
|
# Initialize run directory (P1)
|
|
ensure_run_dir || {
|
|
error "Failed to initialize run directory"
|
|
exit 1
|
|
}
|
|
|
|
# Phase 1 specific configuration
|
|
VERIFY_FIRE=${VERIFY_FIRE:-false} # Set VERIFY_FIRE=true to enable alarm fire verification
|
|
APP_DIR="${SCRIPT_DIR}"
|
|
PROJECT_ROOT="$(cd "${APP_DIR}/../.." && pwd)"
|
|
|
|
# Allow selecting specific tests on the command line (e.g. ./test-phase1.sh 1 2)
|
|
SELECTED_TESTS=()
|
|
|
|
check_plugin_configured() {
|
|
print_info "Checking if plugin is already configured..."
|
|
|
|
# Wait a moment for app to fully load
|
|
sleep 3
|
|
|
|
# Check if database exists (indicates plugin has been used)
|
|
DB_EXISTS=$($ADB_BIN shell run-as "${APP_ID}" ls databases/ 2>/dev/null | grep -c "daily_notification" || echo "0")
|
|
|
|
# Check if SharedPreferences has configuration (more reliable)
|
|
# The plugin stores config in SharedPreferences
|
|
PREFS_EXISTS=$($ADB_BIN shell run-as "${APP_ID}" ls shared_prefs/ 2>/dev/null | grep -c "DailyNotification" || echo "0")
|
|
|
|
# Check recent logs for configuration activity
|
|
RECENT_CONFIG=$($ADB_BIN logcat -d -t 50 | grep -E "Plugin configured|configurePlugin|Configuration" | tail -3)
|
|
|
|
if [ "${DB_EXISTS}" -gt "0" ] || [ "${PREFS_EXISTS}" -gt "0" ]; then
|
|
print_success "Plugin appears to be configured (database or preferences exist)"
|
|
|
|
# Show user what to check in UI
|
|
print_info "Please verify in the app UI that you see:"
|
|
echo " ⚙️ Plugin Settings: ✅ Configured"
|
|
echo " 🔌 Native Fetcher: ✅ Configured"
|
|
echo ""
|
|
echo "If both show ✅, the plugin is configured and you can skip configuration."
|
|
echo "If either shows ❌ or 'Not configured', you'll need to click 'Configure Plugin'."
|
|
echo ""
|
|
|
|
return 0
|
|
else
|
|
print_info "Plugin not configured (no database or preferences found)"
|
|
print_info "You will need to click 'Configure Plugin' in the app UI"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
check_permissions() {
|
|
print_info "Checking notification permissions..."
|
|
|
|
# Check if POST_NOTIFICATIONS permission is granted (Android 13+)
|
|
PERM_CHECK=$($ADB_BIN shell dumpsys package "${APP_ID}" | grep -A 5 "granted=true" | grep -c "android.permission.POST_NOTIFICATIONS" || echo "0")
|
|
|
|
# Also check via app's permission status if available
|
|
PERM_GRANTED=false
|
|
if [ "${PERM_CHECK}" -gt "0" ]; then
|
|
PERM_GRANTED=true
|
|
else
|
|
# Check if we're on Android 12 or below (permission not required)
|
|
SDK_VERSION=$($ADB_BIN shell getprop ro.build.version.sdk)
|
|
if [ "${SDK_VERSION}" -lt "33" ]; then
|
|
PERM_GRANTED=true # Pre-Android 13 doesn't need runtime permission
|
|
fi
|
|
fi
|
|
|
|
if [ "${PERM_GRANTED}" = true ]; then
|
|
print_success "Notification permissions granted"
|
|
return 0
|
|
else
|
|
print_warn "Notification permissions may not be granted"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
ensure_permissions() {
|
|
if check_permissions; then
|
|
print_success "Permissions already granted"
|
|
return 0
|
|
else
|
|
print_info "Notification permissions needed"
|
|
wait_for_ui_action "In the app UI, click the 'Request Permissions' button.
|
|
|
|
This will show a system permission dialog.
|
|
|
|
Steps:
|
|
1. Click 'Request Permissions' button
|
|
2. In the system dialog, tap 'Allow' to grant notification permission
|
|
3. Return to the app and verify the status shows:
|
|
- 🔔 Notifications: ✅ Granted (or similar)
|
|
|
|
Once permission is granted, press Enter to continue."
|
|
|
|
# Re-check permissions
|
|
sleep 2
|
|
if check_permissions; then
|
|
print_success "Permissions granted"
|
|
return 0
|
|
else
|
|
print_warn "Permissions may still not be granted - continuing anyway"
|
|
print_info "If notifications fail, you may need to grant permissions manually"
|
|
return 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
ensure_plugin_configured() {
|
|
if check_plugin_configured; then
|
|
# Plugin might be configured, but let user verify
|
|
wait_for_ui_action "Please check the Plugin Status section at the top of the app.
|
|
|
|
If you see:
|
|
- ⚙️ Plugin Settings: ✅ Configured
|
|
- 🔌 Native Fetcher: ✅ Configured
|
|
- 🔔 Notifications: ✅ Granted (or similar)
|
|
|
|
Then the plugin is already configured - just press Enter to continue.
|
|
|
|
If any show ❌ or 'Not configured':
|
|
- Click 'Request Permissions' if notifications are not granted
|
|
- Click 'Configure Plugin' if settings/fetcher are not configured
|
|
- Wait for all to show ✅, then press Enter."
|
|
|
|
# Give a moment for any configuration that just happened
|
|
sleep 2
|
|
print_success "Continuing with tests (plugin configuration verified or skipped)"
|
|
return 0
|
|
else
|
|
# Plugin definitely needs configuration
|
|
print_info "Plugin needs configuration"
|
|
|
|
# First ensure permissions
|
|
ensure_permissions
|
|
|
|
wait_for_ui_action "Click the 'Configure Plugin' button in the app UI.
|
|
|
|
Wait for the status to update:
|
|
- ⚙️ Plugin Settings: Should change to ✅ Configured
|
|
- 🔌 Native Fetcher: Should change to ✅ Configured
|
|
|
|
Once both show ✅, press Enter to continue."
|
|
|
|
# Verify configuration completed
|
|
sleep 2
|
|
print_success "Plugin configuration completed (or verified)"
|
|
fi
|
|
}
|
|
|
|
# kill_app is now provided by the library, but Phase 1 uses it with PACKAGE variable
|
|
# The library version uses APP_ID, which is set to PACKAGE, so it should work
|
|
# But we keep this as a compatibility wrapper if needed
|
|
|
|
# Phase 1 specific helper: get_current_time (used for fire verification)
|
|
get_current_time() {
|
|
$ADB_BIN shell date +%s
|
|
}
|
|
|
|
# ========================================
|
|
# CLI Mode Parsing (P2.1)
|
|
# ========================================
|
|
|
|
parse_cli_args() {
|
|
local mode="all"
|
|
SELECTED_TESTS=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--setup)
|
|
mode="setup"
|
|
shift
|
|
;;
|
|
--run)
|
|
mode="run"
|
|
shift
|
|
;;
|
|
--smoke)
|
|
mode="smoke"
|
|
shift
|
|
;;
|
|
--all)
|
|
mode="all"
|
|
shift
|
|
;;
|
|
--ci)
|
|
mode="ci"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
[0-9]*)
|
|
SELECTED_TESTS+=("$1")
|
|
shift
|
|
;;
|
|
*)
|
|
error "Unknown option: $1"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
echo "$mode"
|
|
}
|
|
|
|
show_help() {
|
|
cat <<EOF
|
|
Usage: $0 [MODE] [TEST_IDS...]
|
|
|
|
Modes:
|
|
--setup Run setup only (permissions, app install, plugin config checks)
|
|
--run Run actual Phase 1 tests (requires setup first)
|
|
--smoke Minimal test: schedule one notification + verify pending
|
|
--all Run setup + all tests (default)
|
|
--ci Non-interactive: automated checks only, fail fast
|
|
|
|
Test Selection:
|
|
[TEST_IDS] Optional test numbers to run (e.g., 0 1 2)
|
|
If omitted, all tests run
|
|
|
|
Examples:
|
|
$0 # Run all (setup + tests)
|
|
$0 --setup # Setup only
|
|
$0 --run # Run tests only
|
|
$0 --smoke # Quick smoke test
|
|
$0 --ci # CI mode (non-interactive)
|
|
$0 --run 1 2 # Run only tests 1 and 2
|
|
$0 --smoke 0 # Run smoke test for test 0
|
|
|
|
EOF
|
|
}
|
|
|
|
# ========================================
|
|
# Setup Mode (P2.1)
|
|
# ========================================
|
|
|
|
run_setup() {
|
|
section "Phase 1 Setup"
|
|
|
|
info "This mode performs pre-flight checks and setup:"
|
|
echo " • ADB device connection"
|
|
echo " • Emulator readiness"
|
|
echo " • App build and installation"
|
|
echo " • Permission checks"
|
|
echo " • Plugin configuration checks"
|
|
echo ""
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
# Pre-flight checks
|
|
section "Pre-Flight Checks"
|
|
require_adb_device
|
|
|
|
# Build and install
|
|
build_app
|
|
install_app
|
|
|
|
# Clear logs
|
|
clear_logs
|
|
|
|
# Launch app for setup checks
|
|
launch_app
|
|
sleep 2
|
|
|
|
# Check permissions
|
|
section "Permission Checks"
|
|
if check_permissions; then
|
|
ok "Notification permissions already granted"
|
|
else
|
|
if [ "${MODE}" = "ci" ]; then
|
|
error "Permissions not granted - cannot proceed in CI mode"
|
|
exit 1
|
|
fi
|
|
ensure_permissions
|
|
fi
|
|
|
|
# Check plugin configuration
|
|
section "Plugin Configuration"
|
|
if check_plugin_configured; then
|
|
ok "Plugin appears to be configured"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
ui_prompt "Please verify in the app UI that you see:
|
|
|
|
1) ⚙️ Plugin Settings: ✅ Configured
|
|
2) 🔌 Native Fetcher: ✅ Configured
|
|
3) 🔔 Notifications: ✅ Granted
|
|
|
|
If all show ✅, press Enter to continue.
|
|
If any show ❌, click 'Configure Plugin' first."
|
|
fi
|
|
else
|
|
if [ "${MODE}" = "ci" ]; then
|
|
error "Plugin not configured - cannot proceed in CI mode"
|
|
exit 1
|
|
fi
|
|
ensure_plugin_configured
|
|
fi
|
|
|
|
ok "Setup complete!"
|
|
|
|
# Capture initial state
|
|
capture_alarms "setup_initial"
|
|
capture_logcat "setup_initial" "DNP" 100
|
|
|
|
evidence_block "phase1_setup"
|
|
}
|
|
|
|
# ========================================
|
|
# Smoke Test Mode (P2.1)
|
|
# ========================================
|
|
|
|
run_smoke_test() {
|
|
section "Phase 1 Smoke Test"
|
|
|
|
info "Minimal test: schedule one notification + verify pending"
|
|
echo "Expected time: 2-4 minutes"
|
|
echo ""
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
# Ensure setup is done
|
|
if ! check_permissions >/dev/null 2>&1; then
|
|
warn "Setup not complete - running setup first..."
|
|
run_setup
|
|
fi
|
|
|
|
# Capture before state
|
|
capture_alarms "smoke_before"
|
|
|
|
# Schedule notification
|
|
section "Smoke Test: Schedule Notification"
|
|
launch_app
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
ui_prompt "1) In the app UI, click 'Test Notification' to schedule a notification for 4 minutes in the future."
|
|
else
|
|
info "CI mode: Assuming notification is scheduled manually or via automation"
|
|
sleep 3
|
|
fi
|
|
|
|
# Wait for alarm to be registered
|
|
sleep 3
|
|
|
|
# Capture after state
|
|
capture_alarms "smoke_after"
|
|
capture_logcat "smoke_after" "DNP-SCHEDULE|DNP-NOTIFY" 50
|
|
|
|
# Verify alarm exists
|
|
local alarm_count
|
|
alarm_count="$(get_plugin_alarm_count)"
|
|
|
|
if [ "${alarm_count}" -eq "1" ]; then
|
|
verdict_pass "phase1_smoke" "Smoke test passed: 1 alarm scheduled"
|
|
elif [ "${alarm_count}" -gt "1" ]; then
|
|
verdict_fail "phase1_smoke" "Smoke test failed: ${alarm_count} alarms found (expected: 1)"
|
|
else
|
|
verdict_warn "phase1_smoke" "Smoke test inconclusive: ${alarm_count} alarms found (expected: 1, may be race condition)"
|
|
fi
|
|
|
|
evidence_block "phase1_smoke"
|
|
}
|
|
|
|
# ========================================
|
|
# Main Test Execution
|
|
# ========================================
|
|
|
|
main() {
|
|
# Parse CLI arguments
|
|
MODE="$(parse_cli_args "$@")"
|
|
|
|
section "Phase 1 Testing Script"
|
|
echo "Mode: ${MODE}"
|
|
echo "Run ID: ${RUN_ID}"
|
|
echo "Evidence directory: $(get_run_dir)"
|
|
echo ""
|
|
|
|
if [ "${MODE}" = "setup" ]; then
|
|
run_setup
|
|
return 0
|
|
elif [ "${MODE}" = "smoke" ]; then
|
|
run_smoke_test
|
|
return 0
|
|
elif [ "${MODE}" = "ci" ]; then
|
|
info "CI mode: Non-interactive, automated checks only"
|
|
run_setup
|
|
# In CI mode, we might want to run a subset of tests
|
|
# For now, just run smoke test
|
|
run_smoke_test
|
|
return 0
|
|
fi
|
|
|
|
# For --run and --all modes, continue with full test suite
|
|
if [ "${MODE}" = "all" ]; then
|
|
info "Running full test suite (setup + tests)"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
run_setup
|
|
elif [ "${MODE}" = "run" ]; then
|
|
info "Running tests only (assuming setup already done)"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
fi
|
|
|
|
# ============================================
|
|
# TEST 0: Daily Rollover (Core Contract Verification)
|
|
# ============================================
|
|
if should_run_test "0" SELECTED_TESTS; then
|
|
section "TEST 0: Daily Rollover Verification"
|
|
|
|
info "Purpose: Verify that after a notification fires, the next day's"
|
|
info " schedule is correctly computed and only ONE alarm exists."
|
|
info ""
|
|
info "Expected time: 5-10 minutes"
|
|
info "Automatable: Partial (requires manual clock manipulation or waiting)"
|
|
echo ""
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
# Capture initial state
|
|
capture_alarms "test0_initial"
|
|
capture_logcat "test0_initial" "DNP" 50
|
|
|
|
substep "Step 1: Schedule a test notification"
|
|
launch_app
|
|
ensure_plugin_configured
|
|
|
|
# Capture pre-schedule state
|
|
capture_alarms "test0_before_schedule"
|
|
capture_screenshot "test0_before_schedule"
|
|
|
|
local initial_count system_count
|
|
initial_count="$(get_plugin_alarm_count)"
|
|
system_count="$(get_system_alarm_count)"
|
|
info "Current notification alarms: ${initial_count} (expected before scheduling: 0)"
|
|
info "System/other alarms: ${system_count} (for context)"
|
|
|
|
if [ "${initial_count}" -eq "0" ] 2>/dev/null; then
|
|
ok "No existing notification alarms (clean state)"
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
ui_prompt "1) In the app UI, schedule a daily notification.
|
|
|
|
For this test, schedule it for a time very soon (e.g., 1-2 minutes from now)
|
|
to observe the rollover behavior.
|
|
|
|
This will schedule:
|
|
• 1 notification alarm (AlarmManager) for the specified time
|
|
• 1 prefetch job (WorkManager) for 2 minutes before that time"
|
|
else
|
|
info "CI mode: Assuming notification is scheduled manually"
|
|
sleep 3
|
|
fi
|
|
|
|
sleep 3 # Give alarm time to be registered
|
|
|
|
# Capture post-schedule state
|
|
capture_alarms "test0_after_schedule"
|
|
capture_screenshot "test0_after_schedule"
|
|
|
|
local post_schedule_count
|
|
post_schedule_count="$(get_plugin_alarm_count)"
|
|
|
|
# Preliminary check (final verdict comes after rollover)
|
|
if [ "${post_schedule_count}" -eq "0" ] 2>/dev/null; then
|
|
warn "Found 0 plugin alarms right after scheduling (may be race condition)"
|
|
info "Alarm may not be visible in dumpsys yet - will verify after rollover"
|
|
elif [ "${post_schedule_count}" -eq "1" ] 2>/dev/null; then
|
|
ok "Found 1 notification alarm (expected: 1) - preliminary check passed"
|
|
else
|
|
warn "Found ${post_schedule_count} notification alarms (expected: 1) - DUPLICATES DETECTED!"
|
|
error "This indicates duplicate NOTIFICATION alarms were created (BUG)"
|
|
show_plugin_alarms_compact
|
|
fi
|
|
initial_count="${post_schedule_count}"
|
|
fi
|
|
|
|
# Show alarm details if we found exactly 1 alarm
|
|
if [ "${initial_count}" -eq "1" ] 2>/dev/null; then
|
|
ok "Single notification alarm scheduled (one per day)"
|
|
|
|
local alarm_details
|
|
alarm_details="$($ADB_BIN shell dumpsys alarm | grep -A 3 "com.timesafari.dailynotification" | grep -A 3 "com.timesafari.daily.NOTIFICATION" | head -10)"
|
|
if [ -n "${alarm_details}" ]; then
|
|
info "Notification alarm details:"
|
|
echo "${alarm_details}" | head -5
|
|
echo ""
|
|
fi
|
|
elif [ "${initial_count}" -gt "1" ] 2>/dev/null; then
|
|
warn "Found ${initial_count} notification alarms (expected: 1) - DUPLICATES DETECTED!"
|
|
fi
|
|
|
|
substep "Step 2: Wait for notification to fire"
|
|
info "To complete this test:"
|
|
echo " 1. Wait for notification to fire (or advance emulator clock)"
|
|
echo " 2. Verify plugin computed next day's time (24 hours later)"
|
|
echo " 3. Verify exactly ONE notification alarm exists for tomorrow"
|
|
echo " 4. Verify no duplicate alarms were created"
|
|
echo ""
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
ui_prompt "2) After the notification fires (or you advance the clock), verify:
|
|
|
|
• Only ONE alarm exists (one per day)
|
|
• Alarm time is for tomorrow (24 hours later)
|
|
• No duplicate alarms were created
|
|
|
|
Press Enter when you've verified this (or to skip this test)."
|
|
else
|
|
info "CI mode: Skipping manual verification step"
|
|
sleep 5 # Give time for rollover if automated
|
|
fi
|
|
|
|
substep "Step 3: Verify rollover state"
|
|
info "Polling for stable alarm count (allowing up to ~10 seconds for Android to settle)..."
|
|
|
|
local post_rollover_count system_final
|
|
post_rollover_count="$(wait_for_stable_plugin_alarm_count 5 2)"
|
|
system_final="$(get_system_alarm_count)"
|
|
|
|
# Capture post-rollover state
|
|
capture_alarms "test0_after_rollover"
|
|
capture_logcat "test0_after_rollover" "DNP-SCHEDULE|DNP-NOTIFY|ROLLOVER" 100
|
|
capture_screenshot "test0_after_rollover"
|
|
|
|
info "Notification alarms after rollover: ${post_rollover_count} (expected: 1)"
|
|
info "System/other alarms: ${system_final} (for context)"
|
|
|
|
# Final verdict
|
|
if [ "${post_rollover_count}" -eq "1" ] 2>/dev/null; then
|
|
verdict_pass "test0_daily_rollover" "Daily rollover created exactly one NOTIFICATION alarm for tomorrow"
|
|
elif [ "${post_rollover_count}" -gt "1" ] 2>/dev/null; then
|
|
error "Daily rollover created ${post_rollover_count} NOTIFICATION alarms (duplicates)"
|
|
show_plugin_alarms_compact
|
|
verdict_fail "test0_daily_rollover" "Duplicate alarms detected after rollover (expected: 1, got: ${post_rollover_count})"
|
|
else
|
|
warn "No NOTIFICATION alarm found after rollover (expected: 1, got: 0)"
|
|
show_plugin_alarms_compact
|
|
verdict_warn "test0_daily_rollover" "Rollover may have failed - no alarm found (expected: 1, got: 0)"
|
|
fi
|
|
|
|
evidence_block "test0_daily_rollover"
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
fi # End of "if should_run_test 0" block
|
|
|
|
# ============================================
|
|
# TEST 1: Force-Stop Recovery - Database Restoration
|
|
# ============================================
|
|
if should_run_test "1" SELECTED_TESTS; then
|
|
section "TEST 1: Force-Stop Recovery - Database Restoration"
|
|
|
|
info "Purpose: Verify that after force-stop (which clears alarms), recovery"
|
|
info " uses the database to rebuild alarms on app relaunch."
|
|
info ""
|
|
info "Expected time: 5-8 minutes"
|
|
info "Automatable: Partial (requires manual force-stop verification)"
|
|
echo ""
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
# Capture initial state
|
|
capture_alarms "test1_initial"
|
|
capture_logcat "test1_initial" "DNP" 50
|
|
|
|
substep "Step 1: Clean start - verify no lingering alarms"
|
|
local lingering_count system_count
|
|
lingering_count="$(get_plugin_alarm_count)"
|
|
system_count="$(get_system_alarm_count)"
|
|
info "Current plugin notification alarms: ${lingering_count}"
|
|
info "System/other alarms: ${system_count} (for context)"
|
|
|
|
local goto_test1_end=false
|
|
|
|
if [ "${lingering_count}" -gt "0" ] 2>/dev/null; then
|
|
warn "Found ${lingering_count} lingering plugin alarm(s) - these will interfere with TEST 1"
|
|
info "TEST 1 needs a clean state (no existing plugin alarms)"
|
|
info "Resetting app state via uninstall + reinstall..."
|
|
|
|
# Uninstall existing app
|
|
info "Uninstalling existing app..."
|
|
set +e
|
|
local uninstall_output uninstall_status
|
|
uninstall_output="$($ADB_BIN uninstall "$APP_ID" 2>&1)"
|
|
uninstall_status=$?
|
|
set -e
|
|
|
|
if [ $uninstall_status -eq 0 ]; then
|
|
ok "App uninstall succeeded (clean slate)"
|
|
else
|
|
if grep -q "DELETE_FAILED_INTERNAL_ERROR" <<<"$uninstall_output"; then
|
|
info "No existing app to uninstall (continuing)"
|
|
else
|
|
warn "App uninstall reported an error: $uninstall_output (continuing anyway)"
|
|
fi
|
|
fi
|
|
|
|
# Reinstall APK
|
|
info "Reinstalling APK..."
|
|
if $ADB_BIN install -r "$APK_PATH" >/dev/null 2>&1; then
|
|
ok "App reinstall succeeded"
|
|
else
|
|
error "App reinstall FAILED - cannot proceed with TEST 1"
|
|
warn "Marking TEST 1 as INCONCLUSIVE due to dirty starting state"
|
|
capture_screenshot "test1_dirty_state_reinstall_failed"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
goto_test1_end=true
|
|
fi
|
|
|
|
if [ "${goto_test1_end}" != "true" ]; then
|
|
# Verify install
|
|
if ! $ADB_BIN shell pm list packages | grep -q "$APP_ID"; then
|
|
error "App not found in package list after reinstall - aborting TEST 1"
|
|
capture_screenshot "test1_dirty_state_verify_failed"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
goto_test1_end=true
|
|
fi
|
|
fi
|
|
|
|
if [ "${goto_test1_end}" != "true" ]; then
|
|
# Clear logs
|
|
info "Clearing logcat buffer after reinstall..."
|
|
$ADB_BIN logcat -c || true
|
|
|
|
# Re-check plugin alarms
|
|
info "Rechecking plugin alarms after reset..."
|
|
sleep 2
|
|
lingering_count="$(get_plugin_alarm_count)"
|
|
info "Plugin alarms after reset: ${lingering_count} (expected: 0)"
|
|
|
|
if [ "${lingering_count}" -ne "0" ] 2>/dev/null; then
|
|
warn "TEST 1 starting with non-zero alarm count even after reset; treating as INCONCLUSIVE"
|
|
info "This may indicate device-specific behavior where alarms persist across uninstall"
|
|
capture_screenshot "test1_unexpected_alarms_after_reset"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
goto_test1_end=true
|
|
fi
|
|
fi
|
|
|
|
if [ "${goto_test1_end}" = "true" ]; then
|
|
warn "TEST 1 skipped due to inability to achieve clean starting state"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
else
|
|
ok "App state reset complete. TEST 1 starting from clean state"
|
|
fi
|
|
else
|
|
ok "No lingering plugin alarms found (clean state)"
|
|
fi
|
|
|
|
# Skip remaining TEST 1 steps if we couldn't achieve clean state
|
|
if [ "${goto_test1_end}" != "true" ]; then
|
|
|
|
substep "Step 2: Schedule a known future alarm"
|
|
launch_app
|
|
ensure_plugin_configured
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
ui_prompt "1) In the app UI, click the 'Test Notification' button.
|
|
|
|
This will schedule ONE notification for 4 minutes in the future.
|
|
(App supports one alarm per day)
|
|
|
|
The alarm will be stored in the database AND scheduled in AlarmManager."
|
|
else
|
|
info "CI mode: Assuming notification is scheduled manually"
|
|
sleep 3
|
|
fi
|
|
|
|
sleep 2
|
|
|
|
# Capture before force-stop state
|
|
capture_alarms "test1_before_force_stop"
|
|
capture_logcat "test1_before_force_stop" "DNP-SCHEDULE|DNP-NOTIFY" 50
|
|
|
|
substep "Step 3: Verify alarm exists in AlarmManager (BEFORE force-stop)"
|
|
local alarm_count_before system_count_before
|
|
alarm_count_before="$(get_plugin_alarm_count)"
|
|
system_count_before="$(get_system_alarm_count)"
|
|
info "Plugin alarms: ${alarm_count_before} (expected: 1)"
|
|
info "System/other alarms: ${system_count_before} (for context)"
|
|
|
|
if [ "${alarm_count_before}" -eq "0" ] 2>/dev/null; then
|
|
error "No plugin alarms found in AlarmManager - cannot test force-stop recovery"
|
|
info "Make sure you clicked 'Test Notification' and wait a moment"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
# Re-check
|
|
alarm_count_before="$(get_plugin_alarm_count)"
|
|
if [ "${alarm_count_before}" -eq "0" ] 2>/dev/null; then
|
|
error "Still no alarms found - aborting test"
|
|
verdict_fail "test1_force_stop_recovery" "No alarms found before force-stop - cannot test recovery"
|
|
evidence_block "test1_force_stop_recovery"
|
|
goto_test1_end=true
|
|
fi
|
|
fi
|
|
|
|
if [ "${alarm_count_before}" -eq "1" ] 2>/dev/null; then
|
|
ok "Single plugin alarm confirmed in AlarmManager (one per day)"
|
|
else
|
|
warn "Found ${alarm_count_before} plugin alarms (expected: 1) - continuing anyway"
|
|
fi
|
|
|
|
# Extract trigger time for later verification
|
|
local alarm_details_before alarm_trigger_ms alarm_trigger_sec alarm_readable
|
|
alarm_details_before="$($ADB_BIN shell dumpsys alarm | grep -A 3 "$APP_ID" | head -10)"
|
|
if [ -n "${alarm_details_before}" ]; then
|
|
info "Alarm details:"
|
|
echo "${alarm_details_before}" | head -5
|
|
echo ""
|
|
fi
|
|
|
|
alarm_trigger_ms=$(echo "${alarm_details_before}" | grep -oE "origWhen [0-9]+" | head -1 | awk '{print $2}')
|
|
if [ -n "${alarm_trigger_ms}" ]; then
|
|
alarm_trigger_sec=$((alarm_trigger_ms / 1000))
|
|
alarm_readable=$(date -d "@${alarm_trigger_sec}" 2>/dev/null || echo "${alarm_trigger_sec}")
|
|
info "Alarm scheduled for: ${alarm_readable} (${alarm_trigger_ms} ms)"
|
|
fi
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
substep "Step 4: Force-stop the app (clears alarms)"
|
|
warn "Force-stop will clear ALL alarms from AlarmManager"
|
|
info "Executing: $ADB_BIN shell am force-stop ${APP_ID}"
|
|
force_stop_app
|
|
sleep 2
|
|
|
|
# Capture after force-stop state
|
|
capture_alarms "test1_after_force_stop"
|
|
|
|
substep "Step 5: Verify alarms are MISSING (cleared by OS)"
|
|
sleep 1
|
|
local alarm_count_after system_count_after
|
|
alarm_count_after="$(get_plugin_alarm_count)"
|
|
system_count_after="$(get_system_alarm_count)"
|
|
info "Plugin alarms after force-stop: ${alarm_count_after} (expected: 0)"
|
|
info "System/other alarms: ${system_count_after} (for context)"
|
|
|
|
if [ "${alarm_count_after}" -eq "0" ] 2>/dev/null; then
|
|
ok "Plugin alarms cleared by force-stop (count: ${alarm_count_after})"
|
|
info "This confirms: Force-stop cleared alarms from AlarmManager"
|
|
else
|
|
warn "Plugin alarms still present after force-stop (count: ${alarm_count_after})"
|
|
info "Some devices/OS versions may not clear alarms on force-stop"
|
|
info "Continuing test anyway - recovery should still work"
|
|
fi
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
substep "Step 6: Relaunch app (triggers recovery from database)"
|
|
clear_logs
|
|
launch_app
|
|
sleep 4 # Give recovery time to run
|
|
|
|
substep "Step 7: Verify recovery rebuilt alarms from database"
|
|
sleep 2
|
|
|
|
# Capture after recovery state
|
|
capture_alarms "test1_after_recovery"
|
|
capture_logcat "test1_after_recovery" "DNP-REACTIVATION" 100
|
|
capture_screenshot "test1_after_recovery"
|
|
|
|
local alarm_count_recovered system_count_recovered
|
|
alarm_count_recovered="$(get_plugin_alarm_count)"
|
|
system_count_recovered="$(get_system_alarm_count)"
|
|
info "Plugin alarms after recovery: ${alarm_count_recovered} (expected: 1)"
|
|
info "System/other alarms: ${system_count_recovered} (for context)"
|
|
|
|
info "Checking recovery logs..."
|
|
check_recovery_logs
|
|
|
|
info "Expected log output:"
|
|
echo " DNP-REACTIVATION: Starting app launch recovery"
|
|
echo " DNP-REACTIVATION: Rescheduled alarm: <id> for <time>"
|
|
echo " DNP-REACTIVATION: Cold start recovery complete: ... rescheduled>=1, ..."
|
|
echo ""
|
|
|
|
# Check recovery logs for rescheduling and recovery source
|
|
local recovery_result rescheduled_count verified_count recovery_source
|
|
recovery_result="$($ADB_BIN logcat -d | grep "Cold start recovery complete\|Boot recovery complete" | tail -1)"
|
|
rescheduled_count=$(echo "${recovery_result}" | grep -oE "rescheduled=[0-9]+" | grep -oE "[0-9]+" || echo "0")
|
|
verified_count=$(echo "${recovery_result}" | grep -oE "verified=[0-9]+" | grep -oE "[0-9]+" || echo "0")
|
|
recovery_source="$($ADB_BIN logcat -d | grep -E "recovery source|from database|DATABASE" | tail -1 || echo "")"
|
|
|
|
# Pass/fail criteria
|
|
local test1_passed=false
|
|
|
|
if [ "${alarm_count_recovered}" -gt "0" ] 2>/dev/null; then
|
|
ok "Alarms restored in AlarmManager (count: ${alarm_count_recovered})"
|
|
if [ "${rescheduled_count}" -gt "0" ] 2>/dev/null; then
|
|
ok "Recovery logs confirm rescheduling (rescheduled=${rescheduled_count})"
|
|
test1_passed=true
|
|
else
|
|
warn "Alarms restored but logs show rescheduled=0"
|
|
info "This might be okay if alarms were verified instead of rescheduled"
|
|
# Still pass if alarms are restored, even if rescheduled=0
|
|
test1_passed=true
|
|
fi
|
|
else
|
|
error "No alarms restored (count: ${alarm_count_recovered})"
|
|
info "Recovery may have failed or alarms were not in database"
|
|
fi
|
|
|
|
# Final verdict
|
|
if [ "${test1_passed}" = "true" ]; then
|
|
verdict_pass "test1_force_stop_recovery" "Recovery successfully rebuilt alarms from database (before: ${alarm_count_before}, after stop: ${alarm_count_after}, after recovery: ${alarm_count_recovered}, rescheduled: ${rescheduled_count})"
|
|
else
|
|
verdict_fail "test1_force_stop_recovery" "Recovery did not rebuild alarms correctly (before: ${alarm_count_before}, after stop: ${alarm_count_after}, after recovery: ${alarm_count_recovered})"
|
|
fi
|
|
|
|
evidence_block "test1_force_stop_recovery"
|
|
|
|
# Optional: Verify alarm fires (controlled by VERIFY_FIRE flag)
|
|
if [ "${VERIFY_FIRE}" = "true" ] && [ -n "${alarm_trigger_ms}" ] && [ "${goto_test1_end}" != "true" ]; then
|
|
substep "Step 8: Verify alarm fires at scheduled time (optional)"
|
|
|
|
local current_time_sec current_time_ms wait_ms wait_sec
|
|
current_time_sec=$(get_current_time)
|
|
current_time_ms=$((current_time_sec * 1000))
|
|
wait_ms=$((alarm_trigger_ms - current_time_ms))
|
|
|
|
if [ "${wait_ms}" -lt 0 ]; then
|
|
warn "Alarm time already passed (${wait_ms} ms ago); skipping fire verification"
|
|
else
|
|
wait_sec=$((wait_ms / 1000))
|
|
|
|
# Clamp upper bound to prevent accidentally waiting 30+ minutes
|
|
if [ "${wait_sec}" -gt 600 ]; then
|
|
warn "Alarm is >10 minutes away (${wait_sec}s); skipping fire verification"
|
|
info "To test fire verification, schedule alarm closer to current time"
|
|
else
|
|
info "Alarm scheduled for: ${alarm_readable}"
|
|
info "Current time: $(date -d "@${current_time_sec}" 2>/dev/null || echo "${current_time_sec}")"
|
|
info "Waiting ~${wait_sec} seconds for alarm to fire..."
|
|
|
|
clear_logs
|
|
sleep ${wait_sec}
|
|
sleep 2
|
|
|
|
info "Checking logs for fired alarm..."
|
|
local alarm_fired
|
|
alarm_fired="$($ADB_BIN logcat -d | grep -E "DNP-RECEIVE|DNP-NOTIFY|DNP-WORK|Alarm fired|Notification displayed" | tail -10)"
|
|
|
|
if [ -n "${alarm_fired}" ]; then
|
|
ok "Alarm fired! Logs:"
|
|
echo "${alarm_fired}"
|
|
else
|
|
warn "No alarm fire logs found"
|
|
info "Check notification tray manually or review recent logs"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
fi # End of "if goto_test1_end != true" block
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
fi # End of "if should_run_test 1" block
|
|
|
|
# ============================================
|
|
# TEST 2: Schedule Update (One-Per-Day Semantics)
|
|
# ============================================
|
|
if should_run_test "2" SELECTED_TESTS; then
|
|
section "TEST 2: Schedule Update Verification"
|
|
|
|
info "Purpose: Verify that updating the schedule time maintains 'one per day' semantics"
|
|
info ""
|
|
info "Expected time: 4-6 minutes"
|
|
info "Automatable: Partial (requires manual schedule update)"
|
|
echo ""
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
# Capture initial state
|
|
capture_alarms "test2_initial"
|
|
capture_logcat "test2_initial" "DNP" 50
|
|
|
|
substep "Step 1: Launch app and verify initial schedule"
|
|
launch_app
|
|
ensure_plugin_configured
|
|
|
|
# Get initial alarm count
|
|
local initial_alarm_count system_alarm_count
|
|
initial_alarm_count="$(get_plugin_alarm_count)"
|
|
system_alarm_count="$(get_system_alarm_count)"
|
|
info "Plugin alarms: ${initial_alarm_count} (expected: 1)"
|
|
info "System/other alarms: ${system_alarm_count} (for context)"
|
|
|
|
if [ "${initial_alarm_count}" -eq "1" ] 2>/dev/null; then
|
|
ok "Initial alarm confirmed (one per day)"
|
|
elif [ "${initial_alarm_count}" -eq "0" ] 2>/dev/null; then
|
|
warn "No initial alarm found - scheduling one first..."
|
|
if [ "${MODE}" != "ci" ]; then
|
|
ui_prompt "1) In the app UI, schedule a daily notification (e.g., click 'Test Notification')."
|
|
else
|
|
info "CI mode: Assuming notification is scheduled manually"
|
|
sleep 2
|
|
fi
|
|
sleep 2
|
|
initial_alarm_count="$(get_plugin_alarm_count)"
|
|
if [ "${initial_alarm_count}" -eq "1" ] 2>/dev/null; then
|
|
ok "Alarm scheduled"
|
|
else
|
|
error "Failed to schedule initial alarm"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
fi
|
|
else
|
|
warn "Found ${initial_alarm_count} plugin alarms (expected: 1) - continuing anyway"
|
|
fi
|
|
|
|
# Capture before update state
|
|
capture_alarms "test2_before_update"
|
|
|
|
substep "Step 2: Update schedule time"
|
|
if [ "${MODE}" != "ci" ]; then
|
|
ui_prompt "2) In the app UI, change the schedule time (e.g., from 06:50 to 07:10)
|
|
and apply the schedule.
|
|
|
|
This should cancel the old alarm and schedule a new one at the new time.
|
|
You should still have exactly 1 alarm (one per day)."
|
|
else
|
|
info "CI mode: Assuming schedule is updated manually"
|
|
sleep 3
|
|
fi
|
|
|
|
sleep 3
|
|
|
|
# Capture after update state
|
|
capture_alarms "test2_after_update"
|
|
capture_logcat "test2_after_update" "DNP-SCHEDULE|DNP-NOTIFY" 50
|
|
|
|
local updated_alarm_count
|
|
updated_alarm_count="$(get_plugin_alarm_count)"
|
|
info "Plugin alarms after update: ${updated_alarm_count} (expected: 1)"
|
|
|
|
local test2_failed=false
|
|
|
|
if [ "${updated_alarm_count}" -eq "1" ] 2>/dev/null; then
|
|
ok "Single alarm confirmed after schedule update (one per day maintained)"
|
|
elif [ "${updated_alarm_count}" -gt "1" ] 2>/dev/null; then
|
|
error "TEST 2 FAILED: Found ${updated_alarm_count} plugin alarms after update (expected: 1)"
|
|
error "Old alarm was NOT canceled - violates 'one per day' semantics"
|
|
info "This indicates the plugin did not clean up existing schedules before creating new one"
|
|
test2_failed=true
|
|
else
|
|
warn "Found ${updated_alarm_count} plugin alarms (expected: 1) - no alarm scheduled after update"
|
|
fi
|
|
|
|
substep "Step 3: Kill app and relaunch (triggers recovery)"
|
|
kill_app
|
|
clear_logs
|
|
launch_app
|
|
|
|
substep "Step 4: Check recovery logs and verify alarm count"
|
|
sleep 3
|
|
|
|
# Capture after recovery state
|
|
capture_alarms "test2_after_recovery"
|
|
capture_logcat "test2_after_recovery" "DNP-REACTIVATION" 100
|
|
|
|
check_recovery_logs
|
|
|
|
info "Expected log output (either):"
|
|
echo " DNP-REACTIVATION: Verified scheduled alarm: <id> at <time>"
|
|
echo " OR"
|
|
echo " DNP-REACTIVATION: Rescheduled missing alarm: <id> at <time>"
|
|
echo " DNP-REACTIVATION: Cold start recovery complete: ..., verified=1 or rescheduled=1, ..."
|
|
echo ""
|
|
|
|
local recovery_result rescheduled_count verified_count alarm_count_after_recovery
|
|
recovery_result="$($ADB_BIN logcat -d | grep "Cold start recovery complete" | tail -1)"
|
|
rescheduled_count=$(echo "${recovery_result}" | grep -oE "rescheduled=[0-9]+" | grep -oE "[0-9]+" || echo "0")
|
|
verified_count=$(echo "${recovery_result}" | grep -oE "verified=[0-9]+" | grep -oE "[0-9]+" || echo "0")
|
|
alarm_count_after_recovery="$(get_plugin_alarm_count)"
|
|
info "Plugin alarms after recovery: ${alarm_count_after_recovery} (expected: 1)"
|
|
|
|
# CRITICAL: Test fails if alarm count > 1 after recovery (violates "one per day")
|
|
if [ "${alarm_count_after_recovery}" -gt "1" ] 2>/dev/null; then
|
|
error "TEST 2 FAILED: Found ${alarm_count_after_recovery} plugin alarms after recovery (expected: 1)"
|
|
error "Multiple schedules in database caused recovery to reschedule duplicates"
|
|
error "This violates 'one per day' semantics - only one notification per day should exist"
|
|
test2_failed=true
|
|
fi
|
|
|
|
# Final verdict
|
|
if [ "${test2_failed}" = "true" ]; then
|
|
verdict_fail "test2_schedule_update" "Multiple alarms detected - violates 'one per day' semantics (after update: ${updated_alarm_count}, after recovery: ${alarm_count_after_recovery})"
|
|
elif [ "${alarm_count_after_recovery}" -eq "1" ] 2>/dev/null; then
|
|
if [ "${rescheduled_count}" -gt "0" ] 2>/dev/null; then
|
|
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (rescheduled: ${rescheduled_count})"
|
|
elif [ "${verified_count}" -gt "0" ] 2>/dev/null; then
|
|
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (verified: ${verified_count})"
|
|
else
|
|
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (no recovery needed)"
|
|
fi
|
|
else
|
|
verdict_warn "test2_schedule_update" "Schedule update result inconclusive (after update: ${updated_alarm_count}, after recovery: ${alarm_count_after_recovery})"
|
|
fi
|
|
|
|
evidence_block "test2_schedule_update"
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
fi # End of "if should_run_test 2" block
|
|
|
|
# ============================================
|
|
# TEST 3: Recovery Timeout
|
|
# ============================================
|
|
if should_run_test "3" SELECTED_TESTS; then
|
|
section "TEST 3: Recovery Timeout"
|
|
|
|
info "Purpose: Verify recovery times out gracefully"
|
|
info ""
|
|
info "Expected time: 2-3 minutes"
|
|
info "Automatable: Yes (code verification only)"
|
|
info "Note: Full test (100+ schedules) requires manual execution"
|
|
echo ""
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
# Capture initial state
|
|
capture_alarms "test3_initial"
|
|
|
|
substep "Step 1: Check recovery timeout implementation"
|
|
local timeout_found timeout_protection_found
|
|
timeout_found=false
|
|
timeout_protection_found=false
|
|
|
|
if grep -q "RECOVERY_TIMEOUT_SECONDS.*2L" "${PROJECT_ROOT}/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt" 2>/dev/null; then
|
|
ok "Timeout is set to 2 seconds"
|
|
timeout_found=true
|
|
else
|
|
error "Timeout not found in code"
|
|
fi
|
|
|
|
if grep -q "withTimeout" "${PROJECT_ROOT}/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt" 2>/dev/null; then
|
|
ok "Timeout protection is implemented"
|
|
timeout_protection_found=true
|
|
else
|
|
error "Timeout protection not found"
|
|
fi
|
|
|
|
# Capture evidence
|
|
capture_logcat "test3_timeout_check" "DNP-REACTIVATION" 50
|
|
|
|
# Final verdict
|
|
if [ "${timeout_found}" = "true" ] && [ "${timeout_protection_found}" = "true" ]; then
|
|
verdict_pass "test3_recovery_timeout" "Timeout mechanism verified in code (2 seconds, withTimeout protection)"
|
|
else
|
|
verdict_fail "test3_recovery_timeout" "Timeout mechanism not properly implemented (timeout found: ${timeout_found}, protection found: ${timeout_protection_found})"
|
|
fi
|
|
|
|
evidence_block "test3_recovery_timeout"
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
fi # End of "if should_run_test 3" block
|
|
|
|
# ============================================
|
|
# TEST 4: Invalid Data Handling
|
|
# ============================================
|
|
if should_run_test "4" SELECTED_TESTS; then
|
|
section "TEST 4: Invalid Data Handling"
|
|
|
|
info "Purpose: Verify invalid data doesn't crash recovery"
|
|
info ""
|
|
info "Expected time: 5-8 minutes"
|
|
info "Automatable: Partial (requires debuggable app or manual injection)"
|
|
info "Note: This test injects invalid data (empty IDs, null nextRunAt) and"
|
|
info " verifies that recovery handles it gracefully without crashing"
|
|
echo ""
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
|
|
# Capture initial state
|
|
capture_alarms "test4_initial"
|
|
capture_logcat "test4_initial" "DNP" 50
|
|
|
|
substep "Step 1: Inject invalid test data"
|
|
|
|
# Clear logs before test
|
|
clear_logs
|
|
|
|
# Check if app is debuggable (look for DEBUGGABLE flag)
|
|
local is_debuggable=false
|
|
if $ADB_BIN shell dumpsys package "${APP_ID}" | grep -qi "DEBUGGABLE"; then
|
|
is_debuggable=true
|
|
fi
|
|
|
|
if [ "${is_debuggable}" = "true" ]; then
|
|
ok "App is debuggable - can inject data via database"
|
|
|
|
substep "Step 2: Inject invalid data via direct database access"
|
|
|
|
# Launch app first to ensure database is initialized
|
|
info "Launching app to initialize database..."
|
|
$ADB_BIN shell am start -n "${APP_ID}/.MainActivity" > /dev/null 2>&1
|
|
sleep 2
|
|
|
|
# Stop app before database injection (prevents locking issues)
|
|
info "Stopping app before database injection..."
|
|
$ADB_BIN shell am force-stop "${APP_ID}"
|
|
sleep 1
|
|
|
|
# Inject invalid data via direct database access
|
|
info "Injecting invalid test data into database..."
|
|
|
|
# Calculate next run time (24 hours from now in milliseconds)
|
|
local next_run past_time now_time
|
|
next_run=$(($(date +%s) * 1000 + 86400000))
|
|
past_time=$(($(date +%s) * 1000 - 3600000))
|
|
now_time=$(($(date +%s) * 1000))
|
|
|
|
# Inject schedule with empty ID
|
|
info " - Injecting schedule with empty ID..."
|
|
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"INSERT OR REPLACE INTO schedules (id, kind, cron, clockTime, enabled, nextRunAt, jitterMs, backoffPolicy) VALUES ('', 'notify', '0 9 * * *', '09:00', 1, ${next_run}, 0, 'exp');\"" 2>&1 || true
|
|
|
|
# Inject schedule with null nextRunAt
|
|
info " - Injecting schedule with null nextRunAt..."
|
|
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"INSERT OR REPLACE INTO schedules (id, kind, cron, clockTime, enabled, nextRunAt, jitterMs, backoffPolicy) VALUES ('test_null_nextrunat', 'notify', '0 9 * * *', '09:00', 1, NULL, 0, 'exp');\"" 2>&1 || true
|
|
|
|
# Checkpoint WAL file to ensure changes are visible
|
|
info " - Checkpointing WAL file..."
|
|
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"PRAGMA wal_checkpoint(FULL);\"" 2>&1 || true
|
|
|
|
# Verify data was inserted
|
|
info "Verifying data injection..."
|
|
local schedule_count
|
|
schedule_count="$($ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"SELECT COUNT(*) FROM schedules;\"" 2>&1 | tr -d '\r\n')"
|
|
|
|
info " - Schedules in database: ${schedule_count}"
|
|
|
|
if [ "${schedule_count}" -gt "0" ] 2>/dev/null; then
|
|
ok "Invalid test data injected successfully"
|
|
else
|
|
warn "No schedules found after injection - data may not have been inserted"
|
|
fi
|
|
|
|
substep "Step 3: Trigger recovery with invalid data"
|
|
$ADB_BIN shell am start -n "${APP_ID}/.MainActivity" > /dev/null 2>&1
|
|
sleep 3 # Give recovery time to run
|
|
|
|
info "Waiting for recovery to complete..."
|
|
sleep 2
|
|
|
|
# Capture after recovery state
|
|
capture_alarms "test4_after_recovery"
|
|
capture_logcat "test4_after_recovery" "DNP-REACTIVATION|Skipping invalid" 100
|
|
capture_screenshot "test4_after_recovery"
|
|
|
|
substep "Step 4: Check recovery logs for invalid data handling"
|
|
|
|
# Check logs
|
|
local recovery_logs
|
|
recovery_logs="$($ADB_BIN logcat -d | grep -E "DNP-REACTIVATION|Skipping invalid|TEST:" | tail -30)"
|
|
|
|
echo ""
|
|
info "Recovery logs:"
|
|
echo "${recovery_logs}"
|
|
echo ""
|
|
|
|
# Check for invalid data handling
|
|
local test4_passed=false
|
|
local test4_failed=false
|
|
|
|
if echo "${recovery_logs}" | grep -q "Skipping invalid"; then
|
|
ok "Invalid data was detected and skipped"
|
|
echo "${recovery_logs}" | grep "Skipping invalid"
|
|
test4_passed=true
|
|
else
|
|
warn "No 'Skipping invalid' logs found"
|
|
info "This could mean:"
|
|
echo " - Invalid data wasn't injected (database constraints prevented it)"
|
|
echo " - Recovery didn't encounter invalid data"
|
|
echo " - Logs were cleared"
|
|
fi
|
|
|
|
if echo "${recovery_logs}" | grep -q "recovery complete\|Recovery completed"; then
|
|
ok "Recovery completed successfully"
|
|
if [ "${test4_passed}" = "false" ]; then
|
|
test4_passed=true # Recovery completed = passed (even if no invalid data found)
|
|
fi
|
|
else
|
|
warn "Recovery completion message not found in logs"
|
|
fi
|
|
|
|
if echo "${recovery_logs}" | grep -qiE "crash|fatal|exception.*recovery|Failed.*recovery"; then
|
|
error "TEST 4 FAILED: Recovery crashed or threw fatal exception"
|
|
test4_failed=true
|
|
fi
|
|
|
|
# Final verdict
|
|
if [ "${test4_failed}" = "true" ]; then
|
|
verdict_fail "test4_invalid_data_handling" "Recovery crashed or threw fatal exception when handling invalid data"
|
|
elif [ "${test4_passed}" = "true" ]; then
|
|
verdict_pass "test4_invalid_data_handling" "Recovery handled invalid data gracefully (or no invalid data found)"
|
|
else
|
|
verdict_warn "test4_invalid_data_handling" "Could not verify invalid data handling"
|
|
fi
|
|
|
|
else
|
|
info "App is not debuggable - cannot inject data via database"
|
|
info "TEST 4: Code review confirms invalid data handling exists"
|
|
info " - ReactivationManager.kt checks for empty IDs"
|
|
info " - Errors are logged but don't crash recovery"
|
|
verdict_warn "test4_invalid_data_handling" "App not debuggable - cannot inject invalid data for testing"
|
|
fi
|
|
|
|
evidence_block "test4_invalid_data_handling"
|
|
|
|
if [ "${MODE}" != "ci" ]; then
|
|
pause
|
|
fi
|
|
fi # End of "if should_run_test 4" block
|
|
|
|
# ============================================
|
|
# Summary
|
|
# ============================================
|
|
section "Testing Complete"
|
|
|
|
info "Test Results Summary:"
|
|
echo ""
|
|
echo "All test verdicts are shown above with evidence locations."
|
|
echo "Review evidence in: $(get_run_dir)"
|
|
echo ""
|
|
|
|
info "All recovery logs:"
|
|
echo ""
|
|
$ADB_BIN logcat -d | grep "$REACTIVATION_TAG" | tail -20
|
|
echo ""
|
|
|
|
ok "Phase 1 testing script complete!"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " - Review evidence in: $(get_run_dir)"
|
|
echo " - Verify all test verdicts above"
|
|
echo " - Check database if needed (debuggable app)"
|
|
echo " - Update documentation with test results"
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|
|
|