fix: first-run breakage (closes #559, #561) + #560 platform-aware dia… #152
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Firmware QEMU Tests (ADR-061) | |
| on: | |
| push: | |
| paths: | |
| - 'firmware/**' | |
| - 'scripts/qemu-esp32s3-test.sh' | |
| - 'scripts/validate_qemu_output.py' | |
| - 'scripts/generate_nvs_matrix.py' | |
| - 'scripts/qemu_swarm.py' | |
| - 'scripts/swarm_health.py' | |
| - 'scripts/swarm_presets/**' | |
| - '.github/workflows/firmware-qemu.yml' | |
| pull_request: | |
| paths: | |
| - 'firmware/**' | |
| - 'scripts/qemu-esp32s3-test.sh' | |
| - 'scripts/validate_qemu_output.py' | |
| - 'scripts/generate_nvs_matrix.py' | |
| - 'scripts/qemu_swarm.py' | |
| - 'scripts/swarm_health.py' | |
| - 'scripts/swarm_presets/**' | |
| - '.github/workflows/firmware-qemu.yml' | |
| env: | |
| IDF_VERSION: "v5.4" | |
| QEMU_REPO: "https://github.com/espressif/qemu.git" | |
| QEMU_BRANCH: "esp-develop" | |
| jobs: | |
| build-qemu: | |
| name: Build Espressif QEMU | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Cache QEMU build | |
| id: cache-qemu | |
| uses: actions/cache@v4 | |
| with: | |
| path: /opt/qemu-esp32 | |
| # Include date component so cache refreshes monthly when branch updates | |
| key: qemu-esp32s3-${{ env.QEMU_BRANCH }}-v5 | |
| restore-keys: | | |
| qemu-esp32s3-${{ env.QEMU_BRANCH }}- | |
| - name: Install QEMU build dependencies | |
| if: steps.cache-qemu.outputs.cache-hit != 'true' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| git build-essential ninja-build pkg-config \ | |
| libglib2.0-dev libpixman-1-dev libslirp-dev \ | |
| libgcrypt20-dev \ | |
| python3 python3-venv | |
| - name: Clone and build Espressif QEMU | |
| if: steps.cache-qemu.outputs.cache-hit != 'true' | |
| run: | | |
| git clone --depth 1 -b "$QEMU_BRANCH" "$QEMU_REPO" /tmp/qemu-esp | |
| cd /tmp/qemu-esp | |
| mkdir build && cd build | |
| ../configure \ | |
| --target-list=xtensa-softmmu \ | |
| --prefix=/opt/qemu-esp32 \ | |
| --enable-slirp \ | |
| --disable-werror | |
| ninja -j$(nproc) | |
| ninja install | |
| - name: Verify QEMU binary | |
| run: | | |
| file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; } | |
| /opt/qemu-esp32/bin/qemu-system-xtensa --version | |
| echo "QEMU binary size: $(file_size /opt/qemu-esp32/bin/qemu-system-xtensa) bytes" | |
| - name: Upload QEMU artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: qemu-esp32 | |
| path: /opt/qemu-esp32/ | |
| retention-days: 7 | |
| qemu-test: | |
| name: QEMU Test (${{ matrix.nvs_config }}) | |
| needs: build-qemu | |
| runs-on: ubuntu-latest | |
| container: | |
| image: espressif/idf:v5.4 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| nvs_config: | |
| - default | |
| - full-adr060 | |
| - edge-tier0 | |
| - edge-tier1 | |
| - tdm-3node | |
| - boundary-max | |
| - boundary-min | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download QEMU artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: qemu-esp32 | |
| path: /opt/qemu-esp32 | |
| - name: Make QEMU executable | |
| run: chmod +x /opt/qemu-esp32/bin/qemu-system-xtensa | |
| - name: Verify QEMU works | |
| run: /opt/qemu-esp32/bin/qemu-system-xtensa --version | |
| - name: Install Python dependencies | |
| run: | | |
| . $IDF_PATH/export.sh | |
| pip install esptool esp-idf-nvs-partition-gen | |
| - name: Set target ESP32-S3 | |
| working-directory: firmware/esp32-csi-node | |
| run: | | |
| . $IDF_PATH/export.sh | |
| idf.py set-target esp32s3 | |
| - name: Build firmware (mock CSI mode) | |
| working-directory: firmware/esp32-csi-node | |
| run: | | |
| . $IDF_PATH/export.sh | |
| idf.py \ | |
| -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" \ | |
| build | |
| - name: Generate NVS matrix | |
| run: | | |
| . $IDF_PATH/export.sh | |
| python3 scripts/generate_nvs_matrix.py \ | |
| --output-dir firmware/esp32-csi-node/build/nvs_matrix \ | |
| --only ${{ matrix.nvs_config }} | |
| - name: Create merged flash image | |
| working-directory: firmware/esp32-csi-node | |
| run: | | |
| . $IDF_PATH/export.sh | |
| # Determine merge_bin arguments | |
| OTA_ARGS="" | |
| if [ -f build/ota_data_initial.bin ]; then | |
| OTA_ARGS="0xf000 build/ota_data_initial.bin" | |
| fi | |
| python3 -m esptool --chip esp32s3 merge_bin \ | |
| -o build/qemu_flash.bin \ | |
| --flash_mode dio --flash_freq 80m --flash_size 8MB \ | |
| --fill-flash-size 8MB \ | |
| 0x0 build/bootloader/bootloader.bin \ | |
| 0x8000 build/partition_table/partition-table.bin \ | |
| $OTA_ARGS \ | |
| 0x20000 build/esp32-csi-node.bin | |
| file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; } | |
| echo "Flash image size: $(file_size build/qemu_flash.bin) bytes" | |
| - name: Inject NVS partition | |
| if: matrix.nvs_config != 'default' | |
| working-directory: firmware/esp32-csi-node | |
| run: | | |
| NVS_BIN="build/nvs_matrix/nvs_${{ matrix.nvs_config }}.bin" | |
| if [ -f "$NVS_BIN" ]; then | |
| file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; } | |
| echo "Injecting NVS: $NVS_BIN ($(file_size "$NVS_BIN") bytes)" | |
| dd if="$NVS_BIN" of=build/qemu_flash.bin \ | |
| bs=1 seek=$((0x9000)) conv=notrunc 2>/dev/null | |
| else | |
| echo "WARNING: NVS binary not found: $NVS_BIN" | |
| fi | |
| - name: Run QEMU smoke test | |
| env: | |
| QEMU_PATH: /opt/qemu-esp32/bin/qemu-system-xtensa | |
| QEMU_TIMEOUT: "90" | |
| run: | | |
| echo "Starting QEMU (timeout: ${QEMU_TIMEOUT}s)..." | |
| timeout "$QEMU_TIMEOUT" "$QEMU_PATH" \ | |
| -machine esp32s3 \ | |
| -nographic \ | |
| -drive file=firmware/esp32-csi-node/build/qemu_flash.bin,if=mtd,format=raw \ | |
| -serial mon:stdio \ | |
| -nic user,model=open_eth,net=10.0.2.0/24 \ | |
| -no-reboot \ | |
| 2>&1 | tee firmware/esp32-csi-node/build/qemu_output.log || true | |
| echo "QEMU finished. Log size: $(wc -l < firmware/esp32-csi-node/build/qemu_output.log) lines" | |
| - name: Validate QEMU output | |
| run: | | |
| python3 scripts/validate_qemu_output.py \ | |
| firmware/esp32-csi-node/build/qemu_output.log | |
| - name: Upload test logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: qemu-logs-${{ matrix.nvs_config }} | |
| path: | | |
| firmware/esp32-csi-node/build/qemu_output.log | |
| firmware/esp32-csi-node/build/nvs_matrix/ | |
| retention-days: 14 | |
| fuzz-test: | |
| name: Fuzz Testing (ADR-061 Layer 6) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install clang | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang | |
| - name: Build fuzz targets | |
| working-directory: firmware/esp32-csi-node/test | |
| run: make all CC=clang | |
| - name: Run serialize fuzzer (60s) | |
| working-directory: firmware/esp32-csi-node/test | |
| run: make run_serialize FUZZ_DURATION=60 || echo "FUZZER_CRASH=serialize" >> "$GITHUB_ENV" | |
| - name: Run edge enqueue fuzzer (60s) | |
| working-directory: firmware/esp32-csi-node/test | |
| run: make run_edge FUZZ_DURATION=60 || echo "FUZZER_CRASH=edge" >> "$GITHUB_ENV" | |
| - name: Run NVS config fuzzer (60s) | |
| working-directory: firmware/esp32-csi-node/test | |
| run: make run_nvs FUZZ_DURATION=60 || echo "FUZZER_CRASH=nvs" >> "$GITHUB_ENV" | |
| - name: Check for crashes | |
| working-directory: firmware/esp32-csi-node/test | |
| run: | | |
| CRASHES=$(find . -type f \( -name "crash-*" -o -name "oom-*" -o -name "timeout-*" \) 2>/dev/null | wc -l) | |
| echo "Crash artifacts found: $CRASHES" | |
| if [ "$CRASHES" -gt 0 ] || [ -n "${FUZZER_CRASH:-}" ]; then | |
| echo "::error::Fuzzer found $CRASHES crash/oom/timeout artifacts. FUZZER_CRASH=${FUZZER_CRASH:-none}" | |
| ls -la crash-* oom-* timeout-* 2>/dev/null | |
| exit 1 | |
| fi | |
| - name: Upload fuzz artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fuzz-crashes | |
| path: | | |
| firmware/esp32-csi-node/test/crash-* | |
| firmware/esp32-csi-node/test/oom-* | |
| firmware/esp32-csi-node/test/timeout-* | |
| retention-days: 30 | |
| nvs-matrix-validate: | |
| name: NVS Matrix Generation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install NVS generator | |
| run: pip install esp-idf-nvs-partition-gen | |
| - name: Generate all 14 NVS configs | |
| run: | | |
| python3 scripts/generate_nvs_matrix.py \ | |
| --output-dir build/nvs_matrix | |
| - name: Verify all binaries generated | |
| run: | | |
| EXPECTED=14 | |
| ACTUAL=$(find build/nvs_matrix -type f -name "nvs_*.bin" 2>/dev/null | wc -l) | |
| echo "Generated $ACTUAL / $EXPECTED NVS binaries" | |
| ls -la build/nvs_matrix/ | |
| if [ "$ACTUAL" -lt "$EXPECTED" ]; then | |
| echo "::error::Only $ACTUAL of $EXPECTED NVS binaries generated" | |
| exit 1 | |
| fi | |
| - name: Verify binary sizes | |
| run: | | |
| file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; } | |
| for f in build/nvs_matrix/nvs_*.bin; do | |
| SIZE=$(file_size "$f") | |
| if [ "$SIZE" -ne 24576 ]; then | |
| echo "::error::$f has unexpected size $SIZE (expected 24576)" | |
| exit 1 | |
| fi | |
| echo " OK: $(basename $f) ($SIZE bytes)" | |
| done | |
| # --------------------------------------------------------------------------- | |
| # ADR-062: QEMU Swarm Configurator Test | |
| # | |
| # Runs a lightweight 3-node swarm (ci_matrix preset) under QEMU to validate | |
| # multi-node orchestration, TDM slot coordination, and swarm-level health | |
| # assertions. Uses the pre-built QEMU binary from the build-qemu job and the | |
| # firmware built by qemu-test. | |
| # | |
| # The CI runner is non-root, so TAP bridge networking is unavailable. | |
| # The orchestrator (qemu_swarm.py) detects this and falls back to SLIRP | |
| # user-mode networking, which is sufficient for the ci_matrix preset. | |
| # --------------------------------------------------------------------------- | |
| swarm-test: | |
| name: Swarm Test (ADR-062) | |
| needs: [build-qemu] | |
| runs-on: ubuntu-latest | |
| container: | |
| image: espressif/idf:v5.4 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download QEMU artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: qemu-esp32 | |
| path: /opt/qemu-esp32 | |
| - name: Make QEMU executable | |
| run: chmod +x /opt/qemu-esp32/bin/qemu-system-xtensa | |
| - name: Install Python dependencies | |
| run: | | |
| . $IDF_PATH/export.sh | |
| pip install pyyaml esptool esp-idf-nvs-partition-gen | |
| - name: Build firmware for swarm | |
| working-directory: firmware/esp32-csi-node | |
| run: | | |
| . $IDF_PATH/export.sh | |
| idf.py set-target esp32s3 | |
| idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" build | |
| python3 -m esptool --chip esp32s3 merge_bin \ | |
| -o build/qemu_flash.bin \ | |
| --flash_mode dio --flash_freq 80m --flash_size 8MB \ | |
| --fill-flash-size 8MB \ | |
| 0x0 build/bootloader/bootloader.bin \ | |
| 0x8000 build/partition_table/partition-table.bin \ | |
| 0x20000 build/esp32-csi-node.bin | |
| - name: Run swarm smoke test | |
| run: | | |
| . $IDF_PATH/export.sh | |
| EXIT_CODE=0 | |
| python3 scripts/qemu_swarm.py --preset ci_matrix \ | |
| --qemu-path /opt/qemu-esp32/bin/qemu-system-xtensa \ | |
| --output-dir build/swarm-results || EXIT_CODE=$? | |
| # Exit 0=PASS, 1=WARN (acceptable in CI without real hardware) | |
| if [ "$EXIT_CODE" -gt 1 ]; then | |
| echo "Swarm test failed with exit code $EXIT_CODE" | |
| exit "$EXIT_CODE" | |
| fi | |
| timeout-minutes: 10 | |
| - name: Upload swarm results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: swarm-results | |
| path: | | |
| build/swarm-results/ | |
| retention-days: 14 |