name: Backend E2E Tests on: push: branches: [master, main] pull_request: branches: [master, main] jobs: # Gate: repository secrets (the build license key) are NOT exposed to # workflow runs triggered by pull_request from a FORK. Without the key the # gated download in the e2e job can only `exit 1`, turning every external # contributor's PR red for a reason unrelated to their change. This tiny job # checks whether the key is present; if not, the real e2e job is SKIPPED # (neutral) instead of failed. Maintainer pushes and same-repo branches — # which do receive the secret — still run the full suite. gate: runs-on: ubuntu-latest outputs: run_e2e: ${{ steps.check.outputs.run_e2e }} steps: - name: Check for build license key (absent on fork PRs) id: check env: VELXIO_LICENSE_KEY: ${{ secrets.VELXIO_BUILD_LICENSE_KEY }} run: | if [ -n "$VELXIO_LICENSE_KEY" ]; then echo "run_e2e=true" >> "$GITHUB_OUTPUT" else echo "run_e2e=false" >> "$GITHUB_OUTPUT" echo "::notice ::VELXIO_BUILD_LICENSE_KEY unavailable (fork PR or unset) — skipping the gated QEMU e2e suite instead of failing it. A maintainer push or same-repo branch runs it in full." fi e2e: needs: gate if: needs.gate.outputs.run_e2e == 'true' runs-on: ubuntu-latest timeout-minutes: 30 env: # velxio.dev gated download endpoint (license module). Override # via the legacy QEMU_RELEASE_URL env if you mirror the binaries # to a private CDN. VELXIO_BINARY_BASE_URL: https://velxio.dev/api/pro/license/downloads QEMU_DIR: /opt/velxio-qemu BACKEND_URL: http://localhost:8001 steps: - name: Checkout repository uses: actions/checkout@v4 # ── System dependencies for libqemu-xtensa.so ──────────────────────────── - name: Install QEMU shared-library dependencies run: | sudo apt-get update -qq sudo apt-get install -y --no-install-recommends \ libfdt1 \ libglib2.0-0 \ libpixman-1-0 \ zlib1g \ libncurses6 \ libslirp0 # ── QEMU binaries + ROM files ────────────────────────────────────────────── # Repository-level secret VELXIO_BUILD_LICENSE_KEY holds a free # personal-tier key generated at https://velxio.dev/license/signup ; # rotate via the admin panel if it leaks. - name: Download QEMU binaries and ROM files env: VELXIO_LICENSE_KEY: ${{ secrets.VELXIO_BUILD_LICENSE_KEY }} run: | if [ -z "$VELXIO_LICENSE_KEY" ]; then echo "::error ::secrets.VELXIO_BUILD_LICENSE_KEY is not set. Add it under Settings → Secrets and variables → Actions. Get a free key from https://velxio.dev/license/signup." exit 1 fi mkdir -p $QEMU_DIR cd $QEMU_DIR # Retry on transient blips: the binaries are served from velxio.dev, # which has brief windows of unavailability during a deploy (the app # container is recreated -> a few seconds of 502). Without retries a # single blip mid-download fails the whole e2e job. --retry-all-errors # covers 5xx (the deploy case), --retry-connrefused the cold-restart # case; 5 x 10s buys ~50s, enough to ride out a container recreate. RETRY="--retry 5 --retry-delay 10 --retry-all-errors --retry-connrefused --connect-timeout 15" echo "Downloading QEMU shared libraries..." curl -fSL $RETRY -o libqemu-xtensa.so "$VELXIO_BINARY_BASE_URL/libqemu-xtensa-amd64?key=$VELXIO_LICENSE_KEY" curl -fSL $RETRY -o libqemu-riscv32.so "$VELXIO_BINARY_BASE_URL/libqemu-riscv32-amd64?key=$VELXIO_LICENSE_KEY" echo "Downloading ROM files..." curl -fSL $RETRY -o esp32-v3-rom.bin "$VELXIO_BINARY_BASE_URL/esp32-v3-rom?key=$VELXIO_LICENSE_KEY" curl -fSL $RETRY -o esp32-v3-rom-app.bin "$VELXIO_BINARY_BASE_URL/esp32-v3-rom-app?key=$VELXIO_LICENSE_KEY" curl -fSL $RETRY -o esp32c3-rom.bin "$VELXIO_BINARY_BASE_URL/esp32c3-rom?key=$VELXIO_LICENSE_KEY" chmod +x libqemu-xtensa.so libqemu-riscv32.so ls -lh . # Report missing shared-library dependencies (if any) echo "=== ldd libqemu-xtensa.so ===" ldd libqemu-xtensa.so || true # ── arduino-cli + ESP32 core + required libraries ────────────────────────── - name: Install arduino-cli run: | curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \ | BINDIR=/usr/local/bin sh - name: Cache arduino-cli cores and libraries uses: actions/cache@v4 with: path: | ~/.arduino15 ~/Arduino/libraries key: arduino-cores-esp32-2x-rp2040-libs-${{ runner.os }}-v2 - name: Install arduino cores and required libraries run: | arduino-cli config init --overwrite # ESP32 core (pinned to 2.x — IDF 4.x compatible with our QEMU build) arduino-cli config add board_manager.additional_urls \ https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json # RP2040 core (Earle Philhower's arduino-pico) arduino-cli config add board_manager.additional_urls \ https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json arduino-cli core update-index # ESP32: Pin to 2.x — the lcgamboa QEMU (esp32-picsimlab) was built against IDF 4.x. # ESP32 Arduino 3.x (IDF 5.x) triggers "Cache disabled" panics on that machine. arduino-cli core install esp32:esp32@2.0.17 # RP2040: used by the Pico MicroPython e2e test compile check arduino-cli core install rp2040:rp2040 # Libraries needed by the e2e test sketches # DHT22 test needs: DHT sensor library # MPU6050 test needs: Adafruit MPU6050 + Adafruit Unified Sensor arduino-cli lib install "DHT sensor library" arduino-cli lib install "Adafruit MPU6050" arduino-cli lib install "Adafruit Unified Sensor" # ── Python + backend dependencies ───────────────────────────────────────── - name: Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: '3.11' - name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-${{ hashFiles('backend/requirements.txt') }} - name: Install backend dependencies run: pip install -r backend/requirements.txt # ── Node.js ─────────────────────────────────────────────────────────────── - name: Set up Node.js 22 uses: actions/setup-node@v4 with: node-version: '22' - name: Install Pico test dependencies (rp2040js) run: npm install --prefix test/backend/e2e # ── Start backend ───────────────────────────────────────────────────────── - name: Start backend server run: | cd backend uvicorn app.main:app --port 8001 --log-level info \ > /tmp/backend.log 2>&1 & echo "Backend PID: $!" env: QEMU_ESP32_LIB: /opt/velxio-qemu/libqemu-xtensa.so QEMU_RISCV32_LIB: /opt/velxio-qemu/libqemu-riscv32.so - name: Wait for backend to be ready run: | echo "Waiting for backend on port 8001..." for i in $(seq 1 60); do if curl -sf http://localhost:8001/health > /dev/null 2>&1; then echo "Backend is up after ${i}s" exit 0 fi sleep 1 done echo "=== Backend failed to start — last 80 lines of log: ===" tail -80 /tmp/backend.log || echo "(no log file)" exit 1 # ── Run e2e tests ───────────────────────────────────────────────────────── # # These tests compile real firmware via arduino-cli and run full simulations # over WebSocket using libqemu-xtensa.so. Each test has a generous timeout # because ESP32 firmware compilation + QEMU boot takes 30-90 s. # - name: Run HC-SR04 e2e test run: node test/backend/e2e/test_hcsr04_simulation.mjs --timeout=75 env: BACKEND_URL: http://localhost:8001 - name: Run DHT22 e2e test run: node test/backend/e2e/test_dht22_simulation.mjs --timeout=90 env: BACKEND_URL: http://localhost:8001 - name: Run MPU-6050 e2e test run: node test/backend/e2e/test_mpu6050_simulation.mjs --timeout=90 env: BACKEND_URL: http://localhost:8001 - name: Run MicroPython Pico e2e test run: node test/backend/e2e/test_micropython_pico.mjs --timeout=180 env: BACKEND_URL: http://localhost:8001 # ── ESP32 + ngspice co-simulation tests ───────────────────────────────── - name: Run ngspice smoke test (no backend needed) run: node test/backend/e2e/test_esp32_spice_smoke.mjs - name: Run ESP32 + ngspice voltage divider co-simulation run: node test/backend/e2e/test_esp32_spice_analog.mjs --timeout=120 env: BACKEND_URL: http://localhost:8001 - name: Run ESP32 + ngspice Wheatstone bridge co-simulation run: node test/backend/e2e/test_esp32_spice_ntc_bridge.mjs --timeout=120 env: BACKEND_URL: http://localhost:8001 - name: Run ESP32 + ngspice photodiode co-simulation run: node test/backend/e2e/test_esp32_spice_photodiode.mjs --timeout=150 env: BACKEND_URL: http://localhost:8001 # ── ESP32-CAM regression test for issue #129 ─────────────────────── # Compiles an IRAM-safe blink sketch via /api/compile/ and asserts # GPIO13 actually toggles in QEMU. Skips automatically if the # backend hasn't come up; runs fully here because the previous # steps already brought it up on port 8001. - name: Install pytest + httpx + websockets run: pip install pytest pytest-asyncio httpx websockets - name: Run ESP32-CAM blink regression test (issue #129) run: pytest test/esp32_cam/ -v --tb=short env: VELXIO_BACKEND_URL: http://localhost:8001 # ── Always dump backend logs so failures are diagnosable ────────────────── - name: Show backend logs if: always() run: | echo "=== Backend log (last 150 lines) ===" tail -150 /tmp/backend.log || echo "(no log file)"