- Add TestEventsPlugin for receiving ADB broadcast intents - Create operator console UI (console/index.html, console.css, console.js) - Add test plan structure (plan.json) with phases, tests, and steps - Wire all test scripts (phase1, phase2, phase3) with step context and events - Add event emission helpers to alarm-test-lib.sh (step_start, step_pass, etc.) - Update test-phase1.sh with comprehensive prerequisite verification - Register TestEventsPlugin in capacitor.plugins.json - Add console documentation (CONSOLE-USAGE.md, CONSOLE-REMAINING-WORK.md) - Add test implementation alignment tracking (TEST-IMPLEMENTATION-ALIGNMENT.md) This enables real-time test progress tracking via structured events from shell scripts to the operator console UI.
1120 lines
32 KiB
Bash
1120 lines
32 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
# ========================================
|
||
# Shared Alarm Test Library
|
||
# ========================================
|
||
#
|
||
# Common helpers for Phase 1, 2, and 3 test scripts
|
||
#
|
||
# Usage: source this file in your test script:
|
||
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# source "${SCRIPT_DIR}/alarm-test-lib.sh"
|
||
#
|
||
# Configuration can be overridden before sourcing:
|
||
# APP_ID="custom.package" source "${SCRIPT_DIR}/alarm-test-lib.sh"
|
||
#
|
||
# STRICT MODE NOTE:
|
||
# This library does NOT set strict mode itself (set -euo pipefail) because
|
||
# it's a library file. Scripts that source this library SHOULD set strict mode:
|
||
# set -euo pipefail
|
||
# IFS=$'\n\t'
|
||
|
||
# --- Config Defaults (can be overridden before sourcing) ---
|
||
|
||
: "${APP_ID:=com.timesafari.dailynotification}"
|
||
: "${APK_PATH:=./app/build/outputs/apk/debug/app-debug.apk}"
|
||
: "${ADB_BIN:=adb}"
|
||
|
||
# Reactivation log tag (common across phases)
|
||
: "${REACTIVATION_TAG:=DNP-REACTIVATION}"
|
||
: "${SCENARIO_KEY:=Detected scenario: }"
|
||
|
||
# Screenshot configuration
|
||
: "${SCREENSHOT_ROOT:=screenshots}"
|
||
: "${ENABLE_SCREENSHOTS:=1}"
|
||
|
||
# Run folder configuration (P1)
|
||
: "${RUN_ID:=$(date '+%Y%m%d_%H%M%S' 2>/dev/null || echo 'unknown')}"
|
||
: "${RUN_DIR:=runs/${RUN_ID}}"
|
||
|
||
# Release gating configuration (P4)
|
||
: "${RELEASE_GATE_PHASE3:=0}"
|
||
|
||
# Derived config (for backward compatibility with Phase 1)
|
||
PACKAGE="${APP_ID}"
|
||
ACTIVITY="${APP_ID}/.MainActivity"
|
||
|
||
# Colors for output (used by Phase 1 print_* functions)
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# ========================================
|
||
# PUBLIC API - UI/Log Helpers
|
||
# ========================================
|
||
# These are the primary functions that all scripts should use.
|
||
# Deprecated functions (print_*, wait_for_*) are kept for backward compatibility.
|
||
|
||
# ========================================
|
||
# Event Emission (Phase B - Live Console Updates)
|
||
# ========================================
|
||
|
||
# Initialize run ID for event emission
|
||
: "${DNP_RUN_ID:=$(date '+%Y%m%d_%H%M%S' 2>/dev/null || echo 'unknown')}"
|
||
|
||
# Emit test event to console via ADB broadcast
|
||
emit_event() {
|
||
# Usage: emit_event TYPE LEVEL [PHASE] [TEST] [STEP] [MESSAGE] [EXTRA_JSON]
|
||
# Example: emit_event "step_start" "INFO" "phase1" "phase1_test0" "p1_t0_s1" "Starting step"
|
||
local event_type="$1"
|
||
local level="${2:-INFO}"
|
||
local phase_id="${3:-}"
|
||
local test_id="${4:-}"
|
||
local step_id="${5:-}"
|
||
local message="${6:-}"
|
||
local extra_json="${7:-}"
|
||
|
||
# Only emit if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
|
||
# Build JSON payload using Python (safer than shell JSON escaping)
|
||
local payload
|
||
payload=$(python3 -c "
|
||
import json
|
||
import sys
|
||
from datetime import datetime
|
||
|
||
event = {
|
||
'version': 'testevent.v1',
|
||
'ts': datetime.now().isoformat(),
|
||
'runId': '${DNP_RUN_ID}',
|
||
'type': '${event_type}',
|
||
'level': '${level}',
|
||
'phaseId': '${phase_id}',
|
||
'testId': '${test_id}',
|
||
'stepId': '${step_id}',
|
||
'message': '${message}'
|
||
}
|
||
|
||
# Add extra JSON fields if provided
|
||
if '${extra_json}':
|
||
try:
|
||
extra = json.loads('${extra_json}')
|
||
event.update(extra)
|
||
except:
|
||
pass
|
||
|
||
print(json.dumps(event))
|
||
" 2>/dev/null)
|
||
|
||
if [ -z "$payload" ]; then
|
||
# Fallback: simple JSON without Python
|
||
payload="{\"version\":\"testevent.v1\",\"ts\":\"$(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S')\",\"runId\":\"${DNP_RUN_ID}\",\"type\":\"${event_type}\",\"level\":\"${level}\",\"phaseId\":\"${phase_id}\",\"testId\":\"${test_id}\",\"stepId\":\"${step_id}\",\"message\":\"${message}\"}"
|
||
fi
|
||
|
||
# Send via ADB broadcast
|
||
adb_broadcast_event "$payload"
|
||
}
|
||
|
||
# Send event via ADB broadcast
|
||
adb_broadcast_event() {
|
||
local payload="$1"
|
||
local action="com.timesafari.dailynotification.TEST_EVENT"
|
||
|
||
# Escape payload for shell (single quotes are safest)
|
||
# Replace single quotes with '\'' (end quote, escaped quote, start quote)
|
||
local escaped_payload
|
||
escaped_payload=$(echo "$payload" | sed "s/'/'\\\\''/g")
|
||
|
||
# Send broadcast
|
||
$ADB_BIN shell am broadcast \
|
||
-a "$action" \
|
||
--es payload "$escaped_payload" \
|
||
>/dev/null 2>&1 || true
|
||
}
|
||
|
||
# Set test context (phase/test/step) for event emission
|
||
set_test_context() {
|
||
# Usage: set_test_context PHASE_ID TEST_ID STEP_ID
|
||
export DNP_PHASE="${1:-}"
|
||
export DNP_TEST="${2:-}"
|
||
export DNP_STEP="${3:-}"
|
||
}
|
||
|
||
# Emit step start event
|
||
step_start() {
|
||
# Usage: step_start STEP_ID [MESSAGE]
|
||
local step_id="${1:-${DNP_STEP}}"
|
||
local message="${2:-Starting step}"
|
||
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
emit_event "step_start" "INFO" "${DNP_PHASE:-}" "${DNP_TEST:-}" "$step_id" "$message"
|
||
fi
|
||
}
|
||
|
||
# Emit step pass event
|
||
step_pass() {
|
||
# Usage: step_pass STEP_ID [MESSAGE]
|
||
local step_id="${1:-${DNP_STEP}}"
|
||
local message="${2:-Step completed}"
|
||
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
emit_event "step_pass" "INFO" "${DNP_PHASE:-}" "${DNP_TEST:-}" "$step_id" "$message"
|
||
fi
|
||
}
|
||
|
||
# Emit step warn event
|
||
step_warn() {
|
||
# Usage: step_warn STEP_ID [MESSAGE]
|
||
local step_id="${1:-${DNP_STEP}}"
|
||
local message="${2:-Step warning}"
|
||
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
emit_event "step_warn" "WARN" "${DNP_PHASE:-}" "${DNP_TEST:-}" "$step_id" "$message"
|
||
fi
|
||
}
|
||
|
||
# Emit step fail event
|
||
step_fail() {
|
||
# Usage: step_fail STEP_ID [MESSAGE]
|
||
local step_id="${1:-${DNP_STEP}}"
|
||
local message="${2:-Step failed}"
|
||
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
emit_event "step_fail" "ERROR" "${DNP_PHASE:-}" "${DNP_TEST:-}" "$step_id" "$message"
|
||
fi
|
||
}
|
||
|
||
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
|
||
|
||
# Emit operator_required event if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
emit_event "operator_required" "WAIT" "${DNP_PHASE:-}" "${DNP_TEST:-}" "${DNP_STEP:-}" "$1"
|
||
fi
|
||
|
||
read -rp "Press Enter after completing the action above..."
|
||
echo
|
||
}
|
||
|
||
# ========================================
|
||
# PUBLIC API - Command Execution Helpers
|
||
# ========================================
|
||
|
||
run_cmd() {
|
||
# Execute a command and capture output
|
||
# Usage: run_cmd "description" command [args...]
|
||
# Returns: exit code of command
|
||
local desc="$1"
|
||
shift
|
||
local cmd=("$@")
|
||
|
||
info "Running: $desc"
|
||
if "${cmd[@]}"; then
|
||
ok "$desc completed"
|
||
return 0
|
||
else
|
||
local exit_code=$?
|
||
error "$desc failed (exit code: $exit_code)"
|
||
return $exit_code
|
||
fi
|
||
}
|
||
|
||
require_cmd() {
|
||
# Execute a command and exit on failure
|
||
# Usage: require_cmd "description" command [args...]
|
||
# Exits script if command fails
|
||
local desc="$1"
|
||
shift
|
||
local cmd=("$@")
|
||
|
||
info "Required: $desc"
|
||
if ! "${cmd[@]}"; then
|
||
local exit_code=$?
|
||
error "$desc failed (exit code: $exit_code)"
|
||
exit $exit_code
|
||
fi
|
||
ok "$desc completed"
|
||
}
|
||
|
||
# ========================================
|
||
# DEPRECATED - Phase 1 Compatibility Aliases
|
||
# ========================================
|
||
# These functions are kept for backward compatibility but should not be used
|
||
# in new code. Use the public API functions above instead.
|
||
|
||
print_header() {
|
||
# DEPRECATED: Use section() instead
|
||
echo ""
|
||
echo -e "${BLUE}========================================${NC}"
|
||
echo -e "${BLUE}$1${NC}"
|
||
echo -e "${BLUE}========================================${NC}"
|
||
echo ""
|
||
}
|
||
|
||
print_step() {
|
||
# DEPRECATED: Use substep() instead
|
||
echo -e "${GREEN}→ Step $1:${NC} $2"
|
||
}
|
||
|
||
print_wait() {
|
||
# DEPRECATED: Use info() or warn() instead
|
||
echo -e "${YELLOW}⏳ $1${NC}"
|
||
}
|
||
|
||
print_success() {
|
||
# DEPRECATED: Use ok() instead
|
||
echo -e "${GREEN}✅ $1${NC}"
|
||
}
|
||
|
||
print_error() {
|
||
# DEPRECATED: Use error() instead
|
||
echo -e "${RED}❌ $1${NC}"
|
||
}
|
||
|
||
print_info() {
|
||
# DEPRECATED: Use info() instead
|
||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||
}
|
||
|
||
print_warn() {
|
||
# DEPRECATED: Use warn() instead
|
||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||
}
|
||
|
||
wait_for_user() {
|
||
# DEPRECATED: Use pause() instead
|
||
echo ""
|
||
read -p "Press Enter when ready to continue..."
|
||
echo ""
|
||
}
|
||
|
||
wait_for_ui_action() {
|
||
# DEPRECATED: Use ui_prompt() instead
|
||
ui_prompt "$1"
|
||
}
|
||
|
||
# --- ADB/Build Helpers ---
|
||
|
||
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 2>/dev/null | grep -q "1"; then
|
||
info "Waiting for emulator to boot..."
|
||
$ADB_BIN wait-for-device
|
||
while [ "$($ADB_BIN shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
|
||
sleep 2
|
||
done
|
||
fi
|
||
ok "Emulator is ready"
|
||
}
|
||
|
||
# Phase 1 compatibility: check_adb_connection + check_emulator_ready
|
||
check_adb_connection() {
|
||
if ! $ADB_BIN devices | grep -q "device$"; then
|
||
print_error "No Android device/emulator connected"
|
||
echo "Please connect a device or start an emulator, then run:"
|
||
echo " adb devices"
|
||
exit 1
|
||
fi
|
||
print_success "ADB device connected"
|
||
}
|
||
|
||
check_emulator_ready() {
|
||
print_info "Checking emulator status..."
|
||
if ! $ADB_BIN shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
|
||
print_wait "Waiting for emulator to boot..."
|
||
$ADB_BIN wait-for-device
|
||
while [ "$($ADB_BIN shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
|
||
sleep 2
|
||
done
|
||
fi
|
||
print_success "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 "Logcat cleared"
|
||
}
|
||
|
||
launch_app() {
|
||
# Check if we're in Phase 1 context (has print_info function)
|
||
if type print_info >/dev/null 2>&1; then
|
||
print_info "Launching app..."
|
||
$ADB_BIN shell am start -n "${ACTIVITY}"
|
||
sleep 3 # Give app time to load and check status
|
||
print_success "App launched"
|
||
else
|
||
substep "Launching app main activity..."
|
||
$ADB_BIN shell am start -n "${ACTIVITY}" >/dev/null 2>&1
|
||
sleep 3 # Give app time to load
|
||
ok "App launched"
|
||
fi
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
# Plugin-specific alarm action (must match AndroidManifest.xml)
|
||
PLUGIN_ALARM_ACTION="com.timesafari.daily.NOTIFICATION"
|
||
|
||
get_plugin_alarm_count() {
|
||
# Returns count of ONLY the plugin's NOTIFICATION alarms (not prefetch - that uses WorkManager)
|
||
# Expected: 1 notification alarm per daily schedule
|
||
#
|
||
# This function counts ALARM_CLOCK wake alarms (RTC_WAKEUP) tagged as:
|
||
# tag=*walarm*:com.timesafari.daily.NOTIFICATION
|
||
#
|
||
# Uses deduplicating parser to avoid double-counting the same alarm that appears in both:
|
||
# - Main alarm list
|
||
# - "Next wake from idle" section (ignored - only counts RTC_WAKEUP blocks)
|
||
# - Alarm Stats section (ignored - only counts actual alarm blocks)
|
||
#
|
||
# Tracks unique Alarm handles to ensure each alarm is counted only once.
|
||
# Checks for app package AND action string anywhere in the block (they appear on different lines).
|
||
local count app_id action
|
||
app_id="$APP_ID"
|
||
action="com.timesafari.daily.NOTIFICATION"
|
||
count="$($ADB_BIN shell dumpsys alarm 2>/dev/null | awk -v app="$app_id" -v action="$action" '
|
||
BEGIN {
|
||
in_block = 0
|
||
alarmId = ""
|
||
hasAppLine = 0
|
||
hasActionLine = 0
|
||
}
|
||
# Start of a new RTC_WAKEUP alarm block
|
||
/^[[:space:]]*RTC_WAKEUP/ {
|
||
# Flush previous block if it was a plugin notification
|
||
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
|
||
seen[alarmId] = 1
|
||
}
|
||
in_block = 1
|
||
alarmId = ""
|
||
hasAppLine = 0
|
||
hasActionLine = 0
|
||
# Extract alarmId from "Alarm{11245c ..."
|
||
if (match($0, /Alarm\{[0-9a-f]+/)) {
|
||
# match is like "Alarm{11245c", extract just the hex part
|
||
alarmId = substr($0, RSTART + 6, RLENGTH - 6)
|
||
}
|
||
}
|
||
# Blank line = end of block
|
||
/^[[:space:]]*$/ {
|
||
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
|
||
seen[alarmId] = 1
|
||
}
|
||
in_block = 0
|
||
alarmId = ""
|
||
hasAppLine = 0
|
||
hasActionLine = 0
|
||
}
|
||
# Lines inside an alarm block
|
||
in_block == 1 {
|
||
if ($0 ~ app) {
|
||
hasAppLine = 1
|
||
}
|
||
if ($0 ~ action) {
|
||
hasActionLine = 1
|
||
}
|
||
}
|
||
END {
|
||
# Flush final block if it was a plugin notification
|
||
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
|
||
seen[alarmId] = 1
|
||
}
|
||
count = 0
|
||
for (id in seen) {
|
||
count++
|
||
}
|
||
print count + 0
|
||
}
|
||
' 2>/dev/null || echo "0")"
|
||
echo "$count" | tr -d '\n\r' | head -1
|
||
}
|
||
|
||
# Alias for backward compatibility
|
||
get_notification_alarm_count() {
|
||
get_plugin_alarm_count
|
||
}
|
||
|
||
get_prefetch_work_count() {
|
||
# Returns count of prefetch WorkManager jobs (not AlarmManager alarms)
|
||
# Note: Prefetch uses WorkManager, not AlarmManager, so it won't appear in dumpsys alarm
|
||
# This is a placeholder - actual WorkManager job counting would require different approach
|
||
local count
|
||
count="$($ADB_BIN shell dumpsys jobscheduler | grep -c "prefetch" 2>/dev/null || echo "0")"
|
||
echo "$count" | tr -d '\n\r' | head -1
|
||
}
|
||
|
||
get_system_alarm_count() {
|
||
# Returns total RTC_WAKEUP alarms on system (for debugging/context)
|
||
local count
|
||
count="$($ADB_BIN shell dumpsys alarm 2>/dev/null | grep -c "RTC_WAKEUP" || echo "0")"
|
||
echo "$count" | tr -d '\n\r' | head -1
|
||
}
|
||
|
||
count_alarms() {
|
||
# Legacy function: now returns plugin-specific alarm count
|
||
# Use get_plugin_alarm_count() for clarity, or get_system_alarm_count() for total
|
||
get_plugin_alarm_count
|
||
}
|
||
|
||
show_plugin_alarms_compact() {
|
||
# Prints only the plugin's alarm block(s) for debugging
|
||
# Shows complete RTC_WAKEUP alarm blocks that contain the app ID
|
||
# This makes it visually obvious why the AWK matcher should pick up alarms
|
||
# (app ID on header line, action on tag line within the same block)
|
||
$ADB_BIN shell dumpsys alarm 2>/dev/null \
|
||
| awk -v app="$APP_ID" '
|
||
BEGIN {
|
||
in_block = 0
|
||
block = ""
|
||
found_app = 0
|
||
}
|
||
/^[[:space:]]*RTC_WAKEUP/ {
|
||
# Print previous block if it contained app ID
|
||
if (in_block && found_app) {
|
||
print block ORS
|
||
}
|
||
# Start new block
|
||
in_block = 1
|
||
block = $0 ORS
|
||
found_app = ($0 ~ app) ? 1 : 0
|
||
next
|
||
}
|
||
/^[[:space:]]*$/ {
|
||
# End of block
|
||
if (in_block && found_app) {
|
||
print block ORS
|
||
}
|
||
in_block = 0
|
||
block = ""
|
||
found_app = 0
|
||
next
|
||
}
|
||
{
|
||
if (in_block) {
|
||
block = block $0 ORS
|
||
if ($0 ~ app) {
|
||
found_app = 1
|
||
}
|
||
}
|
||
}
|
||
END {
|
||
if (in_block && found_app) {
|
||
print block
|
||
}
|
||
}
|
||
' \
|
||
| sed -n '1,80p' || true
|
||
}
|
||
|
||
wait_for_stable_plugin_alarm_count() {
|
||
# Polls for plugin alarm count to stabilize (reduces race condition false negatives)
|
||
# Usage: wait_for_stable_plugin_alarm_count [attempts] [delay_seconds]
|
||
# Default: 5 attempts, 2 second delay (total ~10 seconds)
|
||
# Returns: alarm count (0 if none found after all attempts)
|
||
local attempts=${1:-5}
|
||
local delay=${2:-2}
|
||
local count=0
|
||
local i
|
||
|
||
for i in $(seq 1 "$attempts"); do
|
||
count="$(get_plugin_alarm_count)"
|
||
if [ "$count" -ge 1 ] 2>/dev/null; then
|
||
echo "$count"
|
||
return 0
|
||
fi
|
||
if [ "$i" -lt "$attempts" ]; then
|
||
sleep "$delay"
|
||
fi
|
||
done
|
||
echo "$count"
|
||
}
|
||
|
||
# --- Screenshot Helpers ---
|
||
|
||
take_screenshot() {
|
||
# Captures a device screenshot and saves it with test name, step, and timestamp
|
||
# Usage: take_screenshot "test_name" "step_name"
|
||
# Example: take_screenshot "phase1_test0_daily_rollover" "before_scheduling"
|
||
local test_name="$1"
|
||
local step_name="$2"
|
||
|
||
# Do nothing if screenshots are disabled
|
||
if [ "$ENABLE_SCREENSHOTS" != "1" ]; then
|
||
return 0
|
||
fi
|
||
|
||
if [ -z "$ADB_BIN" ]; then
|
||
echo "⚠️ ADB_BIN is not set; cannot take screenshot." >&2
|
||
return 0
|
||
fi
|
||
|
||
# Timestamp for uniqueness
|
||
local ts
|
||
ts="$(date '+%Y%m%d-%H%M%S' 2>/dev/null)" || ts="unknown"
|
||
|
||
# Directory: screenshots/<test_name>/
|
||
# Use absolute path relative to script directory if SCREENSHOT_ROOT is relative
|
||
local dir
|
||
if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR" ]; then
|
||
dir="${SCRIPT_DIR}/${SCREENSHOT_ROOT}/${test_name}"
|
||
elif [ -n "${BASH_SOURCE[0]}" ]; then
|
||
# Fallback: derive from this script's location
|
||
local lib_dir
|
||
lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || lib_dir=""
|
||
if [ -n "$lib_dir" ]; then
|
||
dir="${lib_dir}/${SCREENSHOT_ROOT}/${test_name}"
|
||
else
|
||
dir="${SCREENSHOT_ROOT}/${test_name}"
|
||
fi
|
||
else
|
||
dir="${SCREENSHOT_ROOT}/${test_name}"
|
||
fi
|
||
mkdir -p "$dir" 2>/dev/null || {
|
||
echo "⚠️ Failed to create screenshot directory: $dir" >&2
|
||
return 0
|
||
}
|
||
|
||
# File name: <test>_<step>_<timestamp>.png, with spaces converted to dashes
|
||
local safe_step="${step_name// /-}"
|
||
local file="${dir}/${test_name}_${safe_step}_${ts}.png"
|
||
|
||
echo "📸 Capturing screenshot: ${file}" >&2
|
||
# Use exec-out to avoid newline mangling
|
||
if ! "$ADB_BIN" exec-out screencap -p > "$file" 2>/dev/null; then
|
||
echo "⚠️ Failed to capture screenshot via adb." >&2
|
||
# Clean up empty file if created
|
||
[ -s "$file" ] || rm -f "$file" 2>/dev/null || true
|
||
return 0
|
||
fi
|
||
|
||
# Verify file was created and has content
|
||
if [ ! -s "$file" ]; then
|
||
echo "⚠️ Screenshot file is empty or missing: $file" >&2
|
||
rm -f "$file" 2>/dev/null || true
|
||
return 0
|
||
fi
|
||
}
|
||
|
||
take_failure_screenshot() {
|
||
# Convenience wrapper for failure cases
|
||
# Usage: take_failure_screenshot "test_name" "reason"
|
||
# Example: take_failure_screenshot "phase1_test0_daily_rollover" "no_alarm_after_rollover"
|
||
local test_name="$1"
|
||
local reason="$2"
|
||
take_screenshot "$test_name" "FAIL_${reason}"
|
||
}
|
||
|
||
force_stop_app() {
|
||
info "Forcing stop of app process..."
|
||
$ADB_BIN shell am force-stop "$APP_ID" || true
|
||
sleep 2
|
||
ok "Force stop issued"
|
||
}
|
||
|
||
# Phase 1 compatibility alias
|
||
kill_app() {
|
||
print_info "Killing app process..."
|
||
$ADB_BIN shell am kill "$APP_ID"
|
||
sleep 2
|
||
|
||
# Verify process is killed
|
||
if $ADB_BIN shell ps | grep -q "$APP_ID"; then
|
||
print_wait "Process still running, using force-stop..."
|
||
$ADB_BIN shell am force-stop "$APP_ID"
|
||
sleep 1
|
||
fi
|
||
|
||
if ! $ADB_BIN shell ps | grep -q "$APP_ID"; then
|
||
print_success "App process terminated"
|
||
else
|
||
print_error "App process still running"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
reboot_emulator() {
|
||
info "Rebooting emulator..."
|
||
$ADB_BIN reboot
|
||
ok "Reboot initiated"
|
||
|
||
info "Waiting for emulator to come back online..."
|
||
$ADB_BIN wait-for-device
|
||
|
||
info "Waiting for system to fully boot..."
|
||
while [ "$($ADB_BIN shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
|
||
sleep 2
|
||
done
|
||
sleep 5 # Additional buffer for system services
|
||
ok "Emulator back online"
|
||
}
|
||
|
||
# --- Log Parsing Helpers ---
|
||
|
||
get_recovery_logs() {
|
||
# Collect only the relevant reactivation logs
|
||
$ADB_BIN logcat -d | grep "$REACTIVATION_TAG" || true
|
||
}
|
||
|
||
extract_field_from_logs() {
|
||
# Usage: extract_field_from_logs "$logs" "rescheduled"
|
||
local logs="$1"
|
||
local key="$2"
|
||
echo "$logs" | grep -E "$key=" | sed -E "s/.*$key=([0-9]+).*/\1/" | tail -n1 || echo "0"
|
||
}
|
||
|
||
extract_scenario_from_logs() {
|
||
local logs="$1"
|
||
echo "$logs" | grep -E "$SCENARIO_KEY" | sed -E "s/.*$SCENARIO_KEY([A-Z_]+).*/\1/" | tail -n1 || echo ""
|
||
}
|
||
|
||
# Phase 1 compatibility: check_recovery_logs
|
||
check_recovery_logs() {
|
||
print_info "Checking recovery logs..."
|
||
echo ""
|
||
$ADB_BIN logcat -d | grep -E "$REACTIVATION_TAG" | tail -10
|
||
echo ""
|
||
}
|
||
|
||
# Phase 1 compatibility: check_alarm_status
|
||
check_alarm_status() {
|
||
print_info "Checking AlarmManager status..."
|
||
echo ""
|
||
$ADB_BIN shell dumpsys alarm | grep -i "$APP_ID" | head -5
|
||
echo ""
|
||
}
|
||
|
||
# --- Test Selection Helper ---
|
||
|
||
should_run_test() {
|
||
# Usage: should_run_test "1" SELECTED_TESTS
|
||
# Returns 0 (success) if test should run, 1 (failure) if not
|
||
local id="$1"
|
||
local -n selected_tests_ref="$2"
|
||
|
||
# If no tests are specified, run all tests
|
||
if [[ "${#selected_tests_ref[@]}" -eq 0 ]]; then
|
||
return 0
|
||
fi
|
||
|
||
for t in "${selected_tests_ref[@]}"; do
|
||
if [[ "$t" == "$id" ]]; then
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# ========================================
|
||
# PUBLIC API - Run Folder & Evidence Helpers (P1)
|
||
# ========================================
|
||
|
||
ensure_run_dir() {
|
||
# Create run directory structure if it doesn't exist
|
||
# Creates: RUN_DIR/logs, RUN_DIR/alarms, RUN_DIR/screens, RUN_DIR/notes
|
||
# Returns: 0 on success, 1 on failure
|
||
local base_dir
|
||
if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR" ]; then
|
||
base_dir="${SCRIPT_DIR}/${RUN_DIR}"
|
||
elif [ -n "${BASH_SOURCE[0]}" ]; then
|
||
local lib_dir
|
||
lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || lib_dir=""
|
||
if [ -n "$lib_dir" ]; then
|
||
base_dir="${lib_dir}/${RUN_DIR}"
|
||
else
|
||
base_dir="${RUN_DIR}"
|
||
fi
|
||
else
|
||
base_dir="${RUN_DIR}"
|
||
fi
|
||
|
||
mkdir -p "${base_dir}/logs" "${base_dir}/alarms" "${base_dir}/screens" "${base_dir}/notes" 2>/dev/null || {
|
||
error "Failed to create run directory: ${base_dir}"
|
||
return 1
|
||
}
|
||
|
||
# Export for use by capture functions
|
||
export RUN_DIR_ABS="${base_dir}"
|
||
return 0
|
||
}
|
||
|
||
get_run_dir() {
|
||
# Get absolute path to current run directory
|
||
# Returns: absolute path, or empty string if not initialized
|
||
echo "${RUN_DIR_ABS:-}"
|
||
}
|
||
|
||
capture_alarms() {
|
||
# Capture AlarmManager dump to run folder
|
||
# Usage: capture_alarms "<label>"
|
||
# Saves to: RUN_DIR/alarms/<label>_alarms.txt
|
||
local label="$1"
|
||
local run_dir
|
||
run_dir="$(get_run_dir)"
|
||
|
||
if [ -z "$run_dir" ]; then
|
||
warn "Run directory not initialized, skipping alarm capture: $label"
|
||
return 0
|
||
fi
|
||
|
||
local safe_label="${label//[^a-zA-Z0-9_-]/_}"
|
||
local file="${run_dir}/alarms/${safe_label}_alarms.txt"
|
||
|
||
info "Capturing alarms: $label → $file"
|
||
if $ADB_BIN shell dumpsys alarm > "$file" 2>/dev/null; then
|
||
ok "Alarms captured: $file"
|
||
|
||
# Emit artifact event if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
local artifact_json="{\"kind\":\"alarms\",\"name\":\"${label}_alarms\",\"path\":\"${file}\"}"
|
||
emit_event "artifact" "EVID" "${DNP_PHASE:-}" "${DNP_TEST:-}" "${DNP_STEP:-}" "Alarms captured: $label" "$artifact_json"
|
||
fi
|
||
|
||
return 0
|
||
else
|
||
warn "Failed to capture alarms: $label"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
capture_logcat() {
|
||
# Capture logcat output to run folder
|
||
# Usage: capture_logcat "<label>" "<grep_pattern>" "<lines>"
|
||
# Saves to: RUN_DIR/logs/<label>_logcat.txt
|
||
# If grep_pattern is empty, captures all recent logs
|
||
local label="$1"
|
||
local pattern="${2:-}"
|
||
local lines="${3:-250}"
|
||
local run_dir
|
||
run_dir="$(get_run_dir)"
|
||
|
||
if [ -z "$run_dir" ]; then
|
||
warn "Run directory not initialized, skipping logcat capture: $label"
|
||
return 0
|
||
fi
|
||
|
||
local safe_label="${label//[^a-zA-Z0-9_-]/_}"
|
||
local file="${run_dir}/logs/${safe_label}_logcat.txt"
|
||
|
||
info "Capturing logcat: $label → $file"
|
||
|
||
if [ -n "$pattern" ]; then
|
||
if $ADB_BIN logcat -d -t "$lines" | grep -E "$pattern" > "$file" 2>/dev/null; then
|
||
ok "Logcat captured (filtered): $file"
|
||
|
||
# Emit artifact event if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
local artifact_json="{\"kind\":\"logs\",\"name\":\"${label}_logcat\",\"path\":\"${file}\"}"
|
||
emit_event "artifact" "EVID" "${DNP_PHASE:-}" "${DNP_TEST:-}" "${DNP_STEP:-}" "Logcat captured: $label" "$artifact_json"
|
||
fi
|
||
|
||
return 0
|
||
else
|
||
# Even if grep finds nothing, create empty file to indicate attempt
|
||
touch "$file"
|
||
warn "No logcat matches for pattern: $pattern"
|
||
return 0
|
||
fi
|
||
else
|
||
if $ADB_BIN logcat -d -t "$lines" > "$file" 2>/dev/null; then
|
||
ok "Logcat captured: $file"
|
||
|
||
# Emit artifact event if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
local artifact_json="{\"kind\":\"logs\",\"name\":\"${label}_logcat\",\"path\":\"${file}\"}"
|
||
emit_event "artifact" "EVID" "${DNP_PHASE:-}" "${DNP_TEST:-}" "${DNP_STEP:-}" "Logcat captured: $label" "$artifact_json"
|
||
fi
|
||
|
||
return 0
|
||
else
|
||
warn "Failed to capture logcat: $label"
|
||
return 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
capture_screenshot() {
|
||
# Capture device screenshot to run folder
|
||
# Usage: capture_screenshot "<label>"
|
||
# Saves to: RUN_DIR/screens/<label>_screenshot.png
|
||
# Falls back to existing take_screenshot() if screenshots enabled
|
||
local label="$1"
|
||
local run_dir
|
||
run_dir="$(get_run_dir)"
|
||
|
||
if [ -z "$run_dir" ]; then
|
||
warn "Run directory not initialized, skipping screenshot: $label"
|
||
return 0
|
||
fi
|
||
|
||
if [ "$ENABLE_SCREENSHOTS" != "1" ]; then
|
||
warn "Screenshots disabled, skipping: $label"
|
||
return 0
|
||
fi
|
||
|
||
local safe_label="${label//[^a-zA-Z0-9_-]/_}"
|
||
local file="${run_dir}/screens/${safe_label}_screenshot.png"
|
||
|
||
info "Capturing screenshot: $label → $file"
|
||
if "$ADB_BIN" exec-out screencap -p > "$file" 2>/dev/null; then
|
||
if [ -s "$file" ]; then
|
||
ok "Screenshot captured: $file"
|
||
|
||
# Emit artifact event if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
local artifact_json="{\"kind\":\"screen\",\"name\":\"${label}_screenshot\",\"path\":\"${file}\"}"
|
||
emit_event "artifact" "EVID" "${DNP_PHASE:-}" "${DNP_TEST:-}" "${DNP_STEP:-}" "Screenshot captured: $label" "$artifact_json"
|
||
fi
|
||
|
||
return 0
|
||
else
|
||
warn "Screenshot file is empty: $file"
|
||
rm -f "$file" 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
else
|
||
warn "Failed to capture screenshot: $label"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
evidence_block() {
|
||
# Print evidence location block for a test
|
||
# Usage: evidence_block "<test_id>"
|
||
# Prints formatted block showing where artifacts are saved
|
||
local test_id="$1"
|
||
local run_dir
|
||
run_dir="$(get_run_dir)"
|
||
|
||
if [ -z "$run_dir" ]; then
|
||
warn "Run directory not initialized, cannot show evidence block"
|
||
return 1
|
||
fi
|
||
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "📦 EVIDENCE: $test_id"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "Run ID: $RUN_ID"
|
||
echo "Evidence directory: $run_dir"
|
||
echo
|
||
echo "Artifacts:"
|
||
echo " • Alarms: $run_dir/alarms/"
|
||
echo " • Logs: $run_dir/logs/"
|
||
echo " • Screens: $run_dir/screens/"
|
||
echo " • Notes: $run_dir/notes/"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
}
|
||
|
||
# ========================================
|
||
# PUBLIC API - Verdict Functions (P1)
|
||
# ========================================
|
||
|
||
verdict_pass() {
|
||
# Emit a PASS verdict for a test
|
||
# Usage: verdict_pass "<test_id>" "<message>"
|
||
local test_id="$1"
|
||
local message="$2"
|
||
local run_dir
|
||
run_dir="$(get_run_dir)"
|
||
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "✅ VERDICT: PASS"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "Test ID: $test_id"
|
||
echo "Status: PASS"
|
||
echo "Message: $message"
|
||
if [ -n "$run_dir" ]; then
|
||
echo "Evidence: $run_dir"
|
||
fi
|
||
echo "Next: Continue to next test"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
|
||
# Emit test_verdict event if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
emit_event "test_verdict" "INFO" "${DNP_PHASE:-}" "$test_id" "" "Test passed: $message"
|
||
fi
|
||
}
|
||
|
||
verdict_warn() {
|
||
# Emit a WARN verdict for a test
|
||
# Usage: verdict_warn "<test_id>" "<message>"
|
||
local test_id="$1"
|
||
local message="$2"
|
||
local run_dir
|
||
run_dir="$(get_run_dir)"
|
||
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "⚠️ VERDICT: WARN"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "Test ID: $test_id"
|
||
echo "Status: WARN"
|
||
echo "Message: $message"
|
||
if [ -n "$run_dir" ]; then
|
||
echo "Evidence: $run_dir"
|
||
fi
|
||
echo "Next: Review evidence and continue"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
|
||
# Emit test_verdict event if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
emit_event "test_verdict" "WARN" "${DNP_PHASE:-}" "$test_id" "" "Test warning: $message"
|
||
fi
|
||
}
|
||
|
||
verdict_fail() {
|
||
# Emit a FAIL verdict for a test
|
||
# Usage: verdict_fail "<test_id>" "<message>"
|
||
# If RELEASE_GATE_PHASE3=1, this will cause script to exit with non-zero
|
||
local test_id="$1"
|
||
local message="$2"
|
||
local run_dir
|
||
run_dir="$(get_run_dir)"
|
||
|
||
echo
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "❌ VERDICT: FAIL"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "Test ID: $test_id"
|
||
echo "Status: FAIL"
|
||
echo "Message: $message"
|
||
if [ -n "$run_dir" ]; then
|
||
echo "Evidence: $run_dir"
|
||
fi
|
||
echo "Next: Review evidence and investigate"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo
|
||
|
||
# Emit test_verdict event if UI events enabled
|
||
if [ "${DNP_UI_EVENTS:-0}" = "1" ]; then
|
||
emit_event "test_verdict" "ERROR" "${DNP_PHASE:-}" "$test_id" "" "Test failed: $message"
|
||
fi
|
||
|
||
# If release gating is enabled, exit with failure
|
||
if [ "${RELEASE_GATE_PHASE3:-0}" = "1" ]; then
|
||
error "Release gating enabled: exiting due to test failure"
|
||
exit 1
|
||
fi
|
||
}
|
||
|