mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 05:59:32 +00:00
9-layer QEMU testing platform (ADR-061) and YAML-driven swarm configurator (ADR-062) for ESP32-S3 firmware testing without hardware. 12 commits, 56 files, +9,500 lines. Tested on Windows with Espressif QEMU 9.0.0 — firmware boots, mock CSI generates frames, 14/16 validation checks pass. 39 bugs found and fixed across 2 deep code reviews. Closes #259 Co-Authored-By: claude-flow <ruv@ruv.net>
212 lines
7.2 KiB
Bash
Executable file
212 lines
7.2 KiB
Bash
Executable file
#!/bin/bash
|
|
# QEMU ESP32-S3 Firmware Test Runner (ADR-061)
|
|
#
|
|
# Builds the firmware with mock CSI enabled, merges binaries into a single
|
|
# flash image, optionally injects a pre-provisioned NVS partition, runs the
|
|
# image under QEMU with a timeout, and validates the UART output.
|
|
#
|
|
# Environment variables:
|
|
# QEMU_PATH - Path to qemu-system-xtensa (default: qemu-system-xtensa)
|
|
# QEMU_TIMEOUT - Timeout in seconds (default: 60)
|
|
# SKIP_BUILD - Set to "1" to skip the idf.py build step
|
|
# NVS_BIN - Path to a pre-built NVS binary to inject (optional)
|
|
#
|
|
# Exit codes:
|
|
# 0 PASS — all checks passed
|
|
# 1 WARN — non-critical checks failed
|
|
# 2 FAIL — critical checks failed
|
|
# 3 FATAL — build error, crash, or infrastructure failure
|
|
|
|
# ── Help ──────────────────────────────────────────────────────────────
|
|
usage() {
|
|
cat <<'HELP'
|
|
Usage: qemu-esp32s3-test.sh [OPTIONS]
|
|
|
|
Build ESP32-S3 firmware with mock CSI, merge binaries into a single flash
|
|
image, run under QEMU with a timeout, and validate the UART output.
|
|
|
|
Options:
|
|
-h, --help Show this help message and exit
|
|
|
|
Environment variables:
|
|
QEMU_PATH Path to qemu-system-xtensa (default: qemu-system-xtensa)
|
|
QEMU_TIMEOUT Timeout in seconds (default: 60)
|
|
SKIP_BUILD Set to "1" to skip idf.py build (default: unset)
|
|
NVS_BIN Path to pre-built NVS binary (optional)
|
|
QEMU_NET Set to "0" to disable networking (default: 1)
|
|
|
|
Examples:
|
|
./qemu-esp32s3-test.sh
|
|
SKIP_BUILD=1 ./qemu-esp32s3-test.sh
|
|
QEMU_PATH=/opt/qemu/bin/qemu-system-xtensa QEMU_TIMEOUT=120 ./qemu-esp32s3-test.sh
|
|
|
|
Exit codes:
|
|
0 PASS — all checks passed
|
|
1 WARN — non-critical checks failed
|
|
2 FAIL — critical checks failed
|
|
3 FATAL — build error, crash, or infrastructure failure
|
|
HELP
|
|
exit 0
|
|
}
|
|
|
|
case "${1:-}" in -h|--help) usage ;; esac
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
FIRMWARE_DIR="$PROJECT_ROOT/firmware/esp32-csi-node"
|
|
BUILD_DIR="$FIRMWARE_DIR/build"
|
|
QEMU_BIN="${QEMU_PATH:-qemu-system-xtensa}"
|
|
FLASH_IMAGE="$BUILD_DIR/qemu_flash.bin"
|
|
LOG_FILE="$BUILD_DIR/qemu_output.log"
|
|
TIMEOUT_SEC="${QEMU_TIMEOUT:-60}"
|
|
|
|
echo "=== QEMU ESP32-S3 Firmware Test (ADR-061) ==="
|
|
echo "Firmware dir: $FIRMWARE_DIR"
|
|
echo "QEMU binary: $QEMU_BIN"
|
|
echo "Timeout: ${TIMEOUT_SEC}s"
|
|
echo ""
|
|
|
|
# ── Prerequisite checks ───────────────────────────────────────────────
|
|
if ! command -v "$QEMU_BIN" &>/dev/null; then
|
|
echo "ERROR: QEMU binary not found: $QEMU_BIN"
|
|
echo " Install: sudo apt install qemu-system-misc # Debian/Ubuntu"
|
|
echo " Install: brew install qemu # macOS"
|
|
echo " Or set QEMU_PATH to the qemu-system-xtensa binary."
|
|
exit 3
|
|
fi
|
|
|
|
if ! command -v python3 &>/dev/null; then
|
|
echo "ERROR: python3 not found."
|
|
echo " Install: sudo apt install python3 # Debian/Ubuntu"
|
|
echo " Install: brew install python # macOS"
|
|
exit 3
|
|
fi
|
|
|
|
if ! python3 -m esptool version &>/dev/null 2>&1; then
|
|
echo "ERROR: esptool not found (needed to merge flash binaries)."
|
|
echo " Install: pip install esptool"
|
|
exit 3
|
|
fi
|
|
|
|
# ── SKIP_BUILD precheck ──────────────────────────────────────────────
|
|
if [ "${SKIP_BUILD:-}" = "1" ] && [ ! -f "$BUILD_DIR/esp32-csi-node.bin" ]; then
|
|
echo "ERROR: SKIP_BUILD=1 but flash image not found: $BUILD_DIR/esp32-csi-node.bin"
|
|
echo "Build the firmware first: ./qemu-esp32s3-test.sh (without SKIP_BUILD)"
|
|
echo "Or unset SKIP_BUILD to build automatically."
|
|
exit 3
|
|
fi
|
|
|
|
# 1. Build with mock CSI enabled (skip if already built)
|
|
if [ "${SKIP_BUILD:-}" != "1" ]; then
|
|
echo "[1/4] Building firmware (mock CSI mode)..."
|
|
idf.py -C "$FIRMWARE_DIR" \
|
|
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" \
|
|
build
|
|
echo ""
|
|
else
|
|
echo "[1/4] Skipping build (SKIP_BUILD=1)"
|
|
echo ""
|
|
fi
|
|
|
|
# Verify build artifacts exist
|
|
for artifact in \
|
|
"$BUILD_DIR/bootloader/bootloader.bin" \
|
|
"$BUILD_DIR/partition_table/partition-table.bin" \
|
|
"$BUILD_DIR/esp32-csi-node.bin"; do
|
|
if [ ! -f "$artifact" ]; then
|
|
echo "ERROR: Build artifact not found: $artifact"
|
|
echo "Run without SKIP_BUILD=1 or build the firmware first."
|
|
exit 3
|
|
fi
|
|
done
|
|
|
|
# 2. Merge binaries into single flash image
|
|
echo "[2/4] Creating merged flash image..."
|
|
|
|
# Check for ota_data_initial.bin; some builds don't produce it
|
|
OTA_DATA_ARGS=""
|
|
if [ -f "$BUILD_DIR/ota_data_initial.bin" ]; then
|
|
OTA_DATA_ARGS="0xf000 $BUILD_DIR/ota_data_initial.bin"
|
|
fi
|
|
|
|
python3 -m esptool --chip esp32s3 merge_bin -o "$FLASH_IMAGE" \
|
|
--flash_mode dio --flash_freq 80m --flash_size 8MB \
|
|
0x0 "$BUILD_DIR/bootloader/bootloader.bin" \
|
|
0x8000 "$BUILD_DIR/partition_table/partition-table.bin" \
|
|
$OTA_DATA_ARGS \
|
|
0x20000 "$BUILD_DIR/esp32-csi-node.bin"
|
|
|
|
echo "Flash image: $FLASH_IMAGE ($(stat -c%s "$FLASH_IMAGE" 2>/dev/null || stat -f%z "$FLASH_IMAGE") bytes)"
|
|
|
|
# 2b. Optionally inject pre-provisioned NVS partition
|
|
NVS_FILE="${NVS_BIN:-$BUILD_DIR/nvs_test.bin}"
|
|
if [ -f "$NVS_FILE" ]; then
|
|
echo "[2b] Injecting NVS partition from: $NVS_FILE"
|
|
# NVS partition offset = 0x9000 = 36864
|
|
dd if="$NVS_FILE" of="$FLASH_IMAGE" \
|
|
bs=1 seek=$((0x9000)) conv=notrunc 2>/dev/null
|
|
echo "NVS injected ($(stat -c%s "$NVS_FILE" 2>/dev/null || stat -f%z "$NVS_FILE") bytes at 0x9000)"
|
|
fi
|
|
echo ""
|
|
|
|
# 3. Run in QEMU with timeout, capture UART output
|
|
echo "[3/4] Running QEMU (timeout: ${TIMEOUT_SEC}s)..."
|
|
echo "------- QEMU UART output -------"
|
|
|
|
# Use timeout command; fall back to gtimeout on macOS
|
|
TIMEOUT_CMD="timeout"
|
|
if ! command -v timeout &>/dev/null; then
|
|
if command -v gtimeout &>/dev/null; then
|
|
TIMEOUT_CMD="gtimeout"
|
|
else
|
|
echo "WARNING: 'timeout' command not found. QEMU may run indefinitely."
|
|
TIMEOUT_CMD=""
|
|
fi
|
|
fi
|
|
|
|
QEMU_EXIT=0
|
|
|
|
# Common QEMU arguments
|
|
QEMU_ARGS=(
|
|
-machine esp32s3
|
|
-nographic
|
|
-drive "file=$FLASH_IMAGE,if=mtd,format=raw"
|
|
-serial mon:stdio
|
|
-no-reboot
|
|
)
|
|
|
|
# Enable SLIRP user-mode networking for UDP if available
|
|
if [ "${QEMU_NET:-1}" != "0" ]; then
|
|
QEMU_ARGS+=(-nic "user,model=open_eth,net=10.0.2.0/24,host=10.0.2.2")
|
|
fi
|
|
|
|
if [ -n "$TIMEOUT_CMD" ]; then
|
|
$TIMEOUT_CMD "$TIMEOUT_SEC" "$QEMU_BIN" "${QEMU_ARGS[@]}" \
|
|
2>&1 | tee "$LOG_FILE" || QEMU_EXIT=$?
|
|
else
|
|
"$QEMU_BIN" "${QEMU_ARGS[@]}" \
|
|
2>&1 | tee "$LOG_FILE" || QEMU_EXIT=$?
|
|
fi
|
|
|
|
echo "------- End QEMU output -------"
|
|
echo ""
|
|
|
|
# timeout returns 124 when the process is killed by timeout — that's expected
|
|
if [ "$QEMU_EXIT" -eq 124 ]; then
|
|
echo "QEMU exited via timeout (expected for firmware that loops forever)."
|
|
elif [ "$QEMU_EXIT" -ne 0 ]; then
|
|
echo "WARNING: QEMU exited with code $QEMU_EXIT"
|
|
fi
|
|
echo ""
|
|
|
|
# 4. Validate expected output
|
|
echo "[4/4] Validating output..."
|
|
python3 "$SCRIPT_DIR/validate_qemu_output.py" "$LOG_FILE"
|
|
VALIDATE_EXIT=$?
|
|
|
|
echo ""
|
|
echo "=== Test Complete (exit code: $VALIDATE_EXIT) ==="
|
|
exit $VALIDATE_EXIT
|