#!/usr/bin/env bash # ============================================================================ # qemu-cli.sh — Unified QEMU ESP32-S3 testing CLI (ADR-061) # Version: 1.0.0 # # Single entry point for all QEMU testing operations. # Run `qemu-cli.sh help` or `qemu-cli.sh --help` for usage. # ============================================================================ set -euo pipefail VERSION="1.0.0" # --- Colors ---------------------------------------------------------------- if [[ -t 1 ]]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; RST='\033[0m' else RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; RST='' fi # --- Resolve paths --------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" FIRMWARE_DIR="$PROJECT_ROOT/firmware/esp32-csi-node" FUZZ_DIR="$FIRMWARE_DIR/test" # --- Helpers --------------------------------------------------------------- info() { echo -e "${BLUE}[INFO]${RST} $*"; } ok() { echo -e "${GREEN}[OK]${RST} $*"; } warn() { echo -e "${YELLOW}[WARN]${RST} $*"; } err() { echo -e "${RED}[ERROR]${RST} $*" >&2; } die() { err "$@"; exit 1; } need_qemu() { detect_qemu >/dev/null 2>&1 || \ die "QEMU not found. Install with: ${CYAN}qemu-cli.sh install${RST}" } detect_qemu() { # 1. Explicit env var if [[ -n "${QEMU_PATH:-}" ]] && [[ -x "$QEMU_PATH" ]]; then echo "$QEMU_PATH"; return 0 fi # 2. On PATH local qemu qemu="$(command -v qemu-system-xtensa 2>/dev/null || true)" if [[ -n "$qemu" ]]; then echo "$qemu"; return 0; fi # 3. Espressif default build location local espressif_qemu="$HOME/.espressif/qemu/build/qemu-system-xtensa" if [[ -x "$espressif_qemu" ]]; then echo "$espressif_qemu"; return 0; fi return 1 } detect_python() { command -v python3 2>/dev/null || command -v python 2>/dev/null || echo "python3" } # --- Command: help --------------------------------------------------------- cmd_help() { cat < [options] ${BOLD}COMMANDS${RST} ${CYAN}install${RST} Install QEMU with ESP32-S3 support ${CYAN}test${RST} Run single-node firmware test ${CYAN}mesh${RST} [N] Run multi-node mesh test (default: 3 nodes) ${CYAN}swarm${RST} [args] Run swarm configurator (qemu_swarm.py) ${CYAN}snapshot${RST} [args] Run snapshot-based tests ${CYAN}chaos${RST} [args] Run chaos / fault injection tests ${CYAN}fuzz${RST} [--duration N] Run all 3 fuzz targets (clang libFuzzer) ${CYAN}nvs${RST} [args] Generate NVS test matrix ${CYAN}health${RST} Check firmware health from QEMU log ${CYAN}status${RST} Show installation status and versions ${CYAN}help${RST} Show this help message ${BOLD}EXAMPLES${RST} qemu-cli.sh install # Install QEMU qemu-cli.sh test # Run basic firmware test qemu-cli.sh test --timeout 120 # Test with longer timeout qemu-cli.sh swarm --preset smoke # Quick swarm test qemu-cli.sh swarm --preset standard # Standard 3-node test qemu-cli.sh swarm --list-presets # List available presets qemu-cli.sh mesh 3 # 3-node mesh test qemu-cli.sh chaos # Run chaos tests qemu-cli.sh fuzz --duration 60 # Fuzz for 60 seconds qemu-cli.sh nvs --list # List NVS configs qemu-cli.sh health build/qemu_output.log qemu-cli.sh status # Show what's installed ${BOLD}TAB COMPLETION${RST} Source the completions in your shell: eval "\$(qemu-cli.sh --completions)" ${BOLD}ENVIRONMENT${RST} QEMU_PATH Path to qemu-system-xtensa binary (auto-detected) FUZZ_DURATION Override fuzz duration in seconds (default: 30) FUZZ_JOBS Parallel fuzzing jobs (default: 1) EOF } # --- Command: install ------------------------------------------------------ cmd_install() { if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then echo "Usage: qemu-cli.sh install" echo "Install QEMU with Espressif ESP32-S3 support." return 0 fi local installer="$SCRIPT_DIR/install-qemu.sh" if [[ -f "$installer" ]]; then info "Running install-qemu.sh ..." bash "$installer" "$@" else info "No install-qemu.sh found. Showing manual install steps." cat </dev/null || true info "Running ${nodes}-node mesh test ..." bash "$SCRIPT_DIR/qemu-mesh-test.sh" "$nodes" "$@" } # --- Command: swarm --------------------------------------------------------- cmd_swarm() { if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then echo "Usage: qemu-cli.sh swarm [--preset NAME] [--list-presets] [args...]" echo "Run QEMU swarm configurator (qemu_swarm.py)." echo "" echo "Presets: smoke, standard, full, stress" echo "List: qemu-cli.sh swarm --list-presets" return 0 fi need_qemu local py; py="$(detect_python)" info "Running swarm configurator ..." "$py" "$SCRIPT_DIR/qemu_swarm.py" "$@" } # --- Command: snapshot ------------------------------------------------------ cmd_snapshot() { if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then echo "Usage: qemu-cli.sh snapshot [args...]" echo "Run snapshot-based QEMU tests." return 0 fi need_qemu info "Running snapshot tests ..." bash "$SCRIPT_DIR/qemu-snapshot-test.sh" "$@" } # --- Command: chaos --------------------------------------------------------- cmd_chaos() { if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then echo "Usage: qemu-cli.sh chaos [args...]" echo "Run chaos / fault injection tests." return 0 fi need_qemu info "Running chaos tests ..." bash "$SCRIPT_DIR/qemu-chaos-test.sh" "$@" } # --- Command: fuzz ---------------------------------------------------------- cmd_fuzz() { local duration="${FUZZ_DURATION:-30}" if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then echo "Usage: qemu-cli.sh fuzz [--duration N]" echo "Build and run all 3 fuzz targets (clang libFuzzer)." echo "Requires: clang with libFuzzer support." return 0 fi while [[ $# -gt 0 ]]; do case "$1" in --duration) duration="$2"; shift 2 ;; *) warn "Unknown fuzz option: $1"; shift ;; esac done if ! command -v clang >/dev/null 2>&1; then die "clang not found. Fuzz targets require clang with libFuzzer." fi info "Building and running fuzz targets (${duration}s each) ..." make -C "$FUZZ_DIR" run_all FUZZ_DURATION="$duration" ok "Fuzz testing complete." } # --- Command: nvs ----------------------------------------------------------- cmd_nvs() { if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then echo "Usage: qemu-cli.sh nvs [--list] [args...]" echo "Generate NVS test configuration matrix." return 0 fi local py; py="$(detect_python)" info "Running NVS matrix generator ..." "$py" "$SCRIPT_DIR/generate_nvs_matrix.py" "$@" } # --- Command: health -------------------------------------------------------- cmd_health() { if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then echo "Usage: qemu-cli.sh health " echo "Analyze firmware health from a QEMU output log." return 0 fi local logfile="${1:-}" if [[ -z "$logfile" ]]; then die "Usage: qemu-cli.sh health " fi if [[ ! -f "$logfile" ]]; then die "Log file not found: $logfile" fi local py; py="$(detect_python)" info "Analyzing health from: $logfile" "$py" "$SCRIPT_DIR/check_health.py" --log "$logfile" --after-fault manual } # --- Command: status -------------------------------------------------------- cmd_status() { # Status should never fail — disable errexit locally set +e echo -e "${BOLD}=== QEMU ESP32-S3 Testing Status ===${RST}" echo "" # QEMU local qemu_bin qemu_bin="$(detect_qemu 2>/dev/null)" if [[ -n "$qemu_bin" ]]; then local qemu_ver qemu_ver="$("$qemu_bin" --version 2>/dev/null | head -1 || echo "unknown")" ok "QEMU: ${GREEN}installed${RST} ($qemu_ver)" echo " Path: $qemu_bin" else warn "QEMU: ${YELLOW}not found${RST} (run: qemu-cli.sh install)" fi # ESP-IDF if [[ -n "${IDF_PATH:-}" ]] && [[ -d "$IDF_PATH" ]]; then ok "ESP-IDF: ${GREEN}available${RST} ($IDF_PATH)" else warn "ESP-IDF: ${YELLOW}IDF_PATH not set${RST}" fi # Python local py; py="$(detect_python)" if command -v "$py" >/dev/null 2>&1; then ok "Python: ${GREEN}$("$py" --version 2>&1)${RST}" else warn "Python: ${YELLOW}not found${RST}" fi # Clang (for fuzz) if command -v clang >/dev/null 2>&1; then ok "Clang: ${GREEN}$(clang --version 2>/dev/null | head -1)${RST}" else warn "Clang: ${YELLOW}not found${RST} (needed for fuzz targets only)" fi # Firmware binary local fw_bin="$FIRMWARE_DIR/build/esp32-csi-node.bin" if [[ -f "$fw_bin" ]]; then local fw_size fw_size="$(stat -c%s "$fw_bin" 2>/dev/null || stat -f%z "$fw_bin" 2>/dev/null || echo "?")" ok "Firmware: ${GREEN}built${RST} ($fw_bin, ${fw_size} bytes)" else warn "Firmware: ${YELLOW}not built${RST} (expected at $fw_bin)" fi # Swarm presets local preset_dir="$SCRIPT_DIR/swarm_presets" if [[ -d "$preset_dir" ]]; then local presets presets="$(ls "$preset_dir"/ 2>/dev/null | \ sed 's/\.\(yaml\|json\)$//' | sort -u | tr '\n' ', ' | sed 's/,$//')" if [[ -n "$presets" ]]; then ok "Presets: ${GREEN}${presets}${RST}" else warn "Presets: ${YELLOW}none found${RST} in $preset_dir" fi fi echo "" set -e } # --- Completions output ----------------------------------------------------- print_completions() { cat <<'COMP' _qemu_cli_completions() { local cmds="install test mesh swarm snapshot chaos fuzz nvs health status help" local cur="${COMP_WORDS[COMP_CWORD]}" if [[ $COMP_CWORD -eq 1 ]]; then COMPREPLY=( $(compgen -W "$cmds" -- "$cur") ) fi } complete -F _qemu_cli_completions qemu-cli.sh COMP } # --- Main dispatch ---------------------------------------------------------- main() { local cmd="${1:-help}" shift 2>/dev/null || true case "$cmd" in install) cmd_install "$@" ;; test) cmd_test "$@" ;; mesh) cmd_mesh "$@" ;; swarm) cmd_swarm "$@" ;; snapshot) cmd_snapshot "$@" ;; chaos) cmd_chaos "$@" ;; fuzz) cmd_fuzz "$@" ;; nvs) cmd_nvs "$@" ;; health) cmd_health "$@" ;; status) cmd_status "$@" ;; help|-h|--help) cmd_help ;; --version) echo "qemu-cli.sh v${VERSION}" ;; --completions) print_completions ;; *) err "Unknown command: ${BOLD}${cmd}${RST}" echo "" cmd_help exit 1 ;; esac } main "$@"