🚧 Add Wokwi ESP32 simulator CI workflow with Playwright web interface testing, boot validation scenarios, combined firmware image support, and network debugging #76
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: Wokwi ESP32 Simulation Test | |
| on: | |
| push: | |
| branches: [ "mdev", "copilot/**" ] | |
| pull_request: | |
| branches: [ "mdev" ] | |
| workflow_dispatch: | |
| jobs: | |
| wokwi-test: | |
| name: Test WLED with Wokwi Simulator | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Cache pip | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip- | |
| - name: Cache PlatformIO | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.platformio | |
| key: ${{ runner.os }}-pio-esp32_V4_wokwi_debug | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.9' | |
| - name: Install PlatformIO | |
| run: pip install -r requirements.txt | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| - name: Install Node.js dependencies | |
| run: npm ci | |
| - name: Build web UI | |
| run: npm run build | |
| - name: Build firmware for ESP32 | |
| env: | |
| WLED_RELEASE: True | |
| run: pio run -e esp32_V4_wokwi_debug | |
| - name: Install Wokwi CLI | |
| run: | | |
| curl -L https://wokwi.com/ci/install.sh | sh | |
| echo "Wokwi CLI installed to: $HOME/.wokwi-ci/bin/" | |
| ls -la "$HOME/.wokwi-ci/bin/" || echo "Directory not found" | |
| export PATH="$HOME/.wokwi-ci/bin:$PATH" | |
| wokwi-cli --version || echo "Warning: wokwi-cli not accessible" | |
| - name: Prepare firmware for Wokwi | |
| run: ./test/wokwi/prepare-firmware.sh esp32_V4_wokwi_debug | |
| - name: Debug - Verify token is set | |
| run: | | |
| if [ -z "$WOKWI_CLI_TOKEN" ]; then | |
| echo "❌ ERROR: WOKWI_CLI_TOKEN is not set" | |
| echo "Please configure WOKWI_CLI_TOKEN as a repository secret" | |
| exit 1 | |
| else | |
| echo "✅ WOKWI_CLI_TOKEN is set (length: ${#WOKWI_CLI_TOKEN} characters)" | |
| fi | |
| env: | |
| WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }} | |
| - name: Quick boot validation with scenario | |
| working-directory: test/wokwi | |
| env: | |
| WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }} | |
| run: | | |
| # Create log directory | |
| mkdir -p logs | |
| # Add Wokwi CLI to PATH for this step | |
| export PATH="$HOME/.wokwi-ci/bin:$PATH" | |
| # Verify combined firmware image exists (recommended approach for Wokwi) | |
| echo "Checking for combined firmware image..." | |
| if [ ! -f "firmware-combined.bin" ]; then | |
| echo "❌ ERROR: firmware-combined.bin not found in $(pwd)" | |
| echo "Available files:" | |
| ls -la | |
| exit 1 | |
| fi | |
| echo "✅ firmware-combined.bin found ($(du -h firmware-combined.bin | cut -f1))" | |
| if [ -f "firmware.elf" ]; then | |
| echo "✅ firmware.elf found ($(du -h firmware.elf | cut -f1))" | |
| else | |
| echo "⚠️ firmware.elf not found (optional for simulation)" | |
| fi | |
| # Verify combined firmware image structure | |
| echo "" | |
| echo "Verifying combined firmware image structure..." | |
| python3 <<'EOF' | |
| import sys | |
| try: | |
| with open('firmware-combined.bin', 'rb') as f: | |
| data = f.read() | |
| size_mb = len(data) / 1024 / 1024 | |
| print(f"✓ Image size: {len(data)} bytes ({size_mb:.2f} MB)") | |
| # Check bootloader at 0x1000 | |
| if len(data) > 0x1000 and data[0x1000] == 0xe9: | |
| print("✓ Bootloader found at 0x1000 (magic byte: 0xe9)") | |
| else: | |
| print("⚠ Bootloader magic byte not found at 0x1000") | |
| sys.exit(1) | |
| # Check partition table at 0x8000 | |
| if len(data) > 0x8000 and data[0x8000:0x8002] == b'\xaa\x50': | |
| print("✓ Partition table found at 0x8000 (magic: 0xaa50)") | |
| else: | |
| print("⚠ Partition table magic not found at 0x8000") | |
| sys.exit(1) | |
| # Check application at 0x10000 | |
| if len(data) > 0x10000 and data[0x10000] == 0xe9: | |
| print("✓ Application found at 0x10000 (magic byte: 0xe9)") | |
| else: | |
| print("⚠ Application magic byte not found at 0x10000") | |
| sys.exit(1) | |
| print("✅ Combined firmware image structure is valid") | |
| except Exception as e: | |
| print(f"❌ ERROR verifying combined image: {e}") | |
| sys.exit(1) | |
| EOF | |
| if [ $? -ne 0 ]; then | |
| echo "❌ Combined firmware image verification failed" | |
| echo "Hex dump of first 256 bytes:" | |
| hexdump -C firmware-combined.bin | head -16 | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "Running quick boot check scenario (15 seconds)..." | |
| echo "Wokwi CLI location: $(which wokwi-cli || echo 'NOT FOUND')" | |
| echo "Wokwi CLI version:" | |
| wokwi-cli --version || echo "Warning: Could not get version" | |
| echo "" | |
| # Run boot check with increased timeout to account for startup time | |
| # Using 30 second timeout for a 15 second scenario to allow for network delays | |
| # Capture serial output (stdout) and Wokwi CLI diagnostics (stderr) separately | |
| if wokwi-cli --timeout 30000 --scenario scenarios/boot-check.yaml . > logs/boot-check-serial.log 2>logs/boot-check.log; then | |
| echo "✅ Boot check passed - firmware boots without crashes" | |
| echo "" | |
| echo "=== Boot check serial output (last 50 lines) ===" | |
| tail -50 logs/boot-check-serial.log | |
| echo "" | |
| echo "=== Boot check CLI log (last 50 lines) ===" | |
| tail -50 logs/boot-check.log | |
| else | |
| EXIT_CODE=$? | |
| echo "❌ Boot check failed with exit code $EXIT_CODE" | |
| echo "" | |
| echo "=== Boot check CLI log ===" | |
| cat logs/boot-check.log | |
| echo "" | |
| echo "=== Boot check serial output ===" | |
| cat logs/boot-check-serial.log | |
| # Check if it's a WebSocket connection error (code 1006) | |
| if grep -q "code 1006" logs/boot-check.log || grep -q "Connection.*closed" logs/boot-check.log; then | |
| echo "" | |
| echo "⚠️ WebSocket connection error detected (code 1006)" | |
| echo "This is typically a transient network issue with Wokwi's API." | |
| echo "The workflow will continue with the full simulation test." | |
| echo "If this persists, check Wokwi service status or network connectivity." | |
| # Don't fail - this might be a transient issue | |
| else | |
| # Other errors should fail the build | |
| exit 1 | |
| fi | |
| fi | |
| - name: Install Playwright browsers | |
| run: npx playwright install --with-deps chromium | |
| - name: Start Wokwi simulator in background | |
| env: | |
| WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }} | |
| run: | | |
| cd test/wokwi | |
| # Create log directory | |
| mkdir -p logs | |
| # Export the token so it's available to child processes | |
| export WOKWI_CLI_TOKEN | |
| # Start simulator in background with a 90 second timeout | |
| # Wokwi CLI outputs to stderr, serial output goes to stdout | |
| ## WOKWI_TIMEOUT=300 ./run-simulator.sh >logs/serial.log 2>&1 & | |
| WOKWI_TIMEOUT=90 ./run-simulator.sh >logs/serial.log 2>&1 & | |
| WOKWI_PID=$! | |
| echo "WOKWI_PID=$WOKWI_PID" >> $GITHUB_ENV | |
| echo "Started Wokwi simulator with PID $WOKWI_PID" | |
| # Wait for simulator to start and web server to be ready | |
| echo "Waiting for WLED web server to be ready..." | |
| #max_wait=240 | |
| max_wait=50 | |
| elapsed=0 | |
| while [ $elapsed -lt $max_wait ]; do | |
| if curl -s -f http://localhost:9080 > /dev/null 2>&1; then | |
| echo "✅ Web server is ready on port 9080 after $elapsed seconds!" | |
| break | |
| fi | |
| if curl -s -f http://localhost:8080 > /dev/null 2>&1; then | |
| echo "✅ Web server is ready on port 8080 after $elapsed seconds!" | |
| break | |
| fi | |
| if ! kill -0 $WOKWI_PID 2>/dev/null; then | |
| echo "❌ Error: Wokwi simulator process died" | |
| echo "" | |
| echo "=== Last 100 lines of Wokwi CLI log ===" | |
| tail -100 logs/wokwi.log || true | |
| echo "" | |
| echo "=== Last 100 lines of Serial output ===" | |
| tail -100 logs/serial.log || true | |
| exit 1 | |
| fi | |
| echo "Still waiting... ($elapsed seconds)" | |
| sleep 5 | |
| elapsed=$((elapsed + 5)) | |
| done | |
| if [ $elapsed -ge $max_wait ]; then | |
| echo "❌ Error: Web server did not start within $max_wait seconds" | |
| echo "" | |
| echo "=== Last 100 lines of Wokwi CLI log ===" | |
| tail -100 logs/wokwi.log || true | |
| echo "" | |
| echo "=== Last 100 lines of Serial output ===" | |
| tail -100 logs/serial.log || true | |
| kill $WOKWI_PID || true | |
| exit 1 | |
| fi | |
| echo "WLED is ready for testing!" | |
| echo "" | |
| echo "=== First 50 lines of Serial output ===" | |
| head -50 logs/serial.log || true | |
| - name: Run Playwright tests | |
| run: npm run test:wokwi | |
| env: | |
| CI: true | |
| - name: Upload Playwright report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: playwright-report | |
| path: playwright-report/ | |
| retention-days: 7 | |
| - name: Stop Wokwi simulator | |
| if: always() | |
| run: | | |
| if [ ! -z "$WOKWI_PID" ]; then | |
| kill $WOKWI_PID || true | |
| fi | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: wokwi-test-results | |
| path: | | |
| test/wokwi/logs/ | |
| test/wokwi/firmware-combined.bin | |
| test-results/ | |
| playwright-report/ | |
| retention-days: 7 |