Files
daily-notification-plugin/test-apps/android-test-app/alarm-test-lib.sh
Matthew Raymer f6df9e13fb feat: add operator console and wire test scripts with event emission
- 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.
2025-12-29 09:37:12 +00:00

1120 lines
32 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ========================================
# 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
}