From 07552b5d9e8150b4dc35809cd3eeb229b9ec5ad4 Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Fri, 15 May 2026 23:19:11 +0200 Subject: [PATCH] =?UTF-8?q?ci:=20Phase=201d-tests=20I=20+=20K=20+=20L=20?= =?UTF-8?q?=E2=80=94=20workflow=20hardening=20+=20nightly=20library-compil?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit K (frontend-tests.yml reinforced): • Matrix node-version: [20, 22] — catches Node-version-specific bugs • Cache the 24 MB ngspice WASM by hash — saves ~10s/run • `npm run tsc` step (continue-on-error: pre-existing strict errors in unrelated test files; tracked but not blocking) • `npm run build` — Vite production build smoke catches Rollup/ Vite-only failures that vitest doesn't see (manualChunks wiring, dynamic import paths, asset resolution) • `npm run test:coverage` + upload as artifact (Node 22 only) L (package.json scripts): • `tsc` → `tsc -b` • `test:libraries` → `RUN_LIBRARY_TESTS=1 vitest run src/__tests__/library-compile.integration.test.ts` I (library-compile nightly): • New `.github/workflows/library-compile.yml` — 5 AM UTC cron + workflow_dispatch. Not on PRs (slow + external deps). • Sets up arduino-cli + caches `~/.arduino15` cores (avr, esp32, rp2040 — ~500 MB). • New `library-compile.integration.test.ts` — iterates every example with `code` + `libraries` + a known FQBN. For each: arduino-cli lib install → write .ino → arduino-cli compile. 7 examples currently match (epaper-displays). • Gated behind RUN_LIBRARY_TESTS=1; default vitest skips the file. Final tally: 1853 tests pass (was 1461 before Phase 1d-tests — +392 new sub-tests across 8 new test files + 1 new workflow). Vite build green (2.68 MB main chunk, unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/frontend-tests.yml | 44 +++++- .github/workflows/library-compile.yml | 74 ++++++++++ frontend/package.json | 6 +- .../library-compile.integration.test.ts | 126 ++++++++++++++++++ 4 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/library-compile.yml create mode 100644 frontend/src/__tests__/library-compile.integration.test.ts diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index bfdc0163..7abb8788 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -9,27 +9,39 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ['20', '22'] steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup Node.js 22 + - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: - node-version: '22' + node-version: ${{ matrix.node-version }} # The metadata generator scans wokwi-elements/src/, which only ships in # the upstream repo (not on npm). Clone it once for the freshness check. - name: Clone wokwi-elements (for metadata regeneration only) run: git clone --depth=1 https://github.com/wokwi/wokwi-elements.git third-party/wokwi-elements + # Cache the vendored ngspice WASM (24 MB). Keyed by the file hash — + # invalidates only when the WASM blob itself changes (rare). Saves + # ~10s of disk write per CI run. + - name: Cache ngspice WASM + uses: actions/cache@v4 + with: + path: frontend/public/wasm/ngspice-interactive + key: ngspice-wasm-${{ hashFiles('frontend/public/wasm/ngspice-interactive/ngspice-lib.wasm') }} + # No node_modules cache. Lock files are gitignored (see .gitignore) so # a cache key tied to package-lock.json never invalidates and ends up # restoring stale links to old `file:` deps from previous commits # (e.g. wokwi-elements pre-npm migration). Fresh install every run is # ~30s slower but actually correct. - - name: Install frontend dependencies run: cd frontend && npm install --no-audit --no-fund --include=optional @@ -49,5 +61,31 @@ jobs: exit 1 fi + - name: TypeScript build + run: cd frontend && npm run tsc + continue-on-error: true # tsc -b has pre-existing strict errors in unrelated test files; tracking separately + - name: Run tests run: cd frontend && npm test + + # Production build smoke — catches Vite/Rollup-only failures that + # vitest doesn't see (chunk wiring, dynamic imports, manualChunks + # config, asset resolution). + - name: Vite production build + run: cd frontend && npm run build + + # Upload coverage as an artifact for download / inspection. Skip + # codecov for now (no org account). Run only on Node 22 to keep the + # artifact list deduped. + - name: Coverage report + if: matrix.node-version == '22' + run: cd frontend && npm run test:coverage + continue-on-error: true + + - name: Upload coverage artifact + if: matrix.node-version == '22' && always() + uses: actions/upload-artifact@v4 + with: + name: coverage-lcov + path: frontend/coverage/ + if-no-files-found: ignore diff --git a/.github/workflows/library-compile.yml b/.github/workflows/library-compile.yml new file mode 100644 index 00000000..109640c5 --- /dev/null +++ b/.github/workflows/library-compile.yml @@ -0,0 +1,74 @@ +name: Library compile (nightly) + +# Phase 1d-tests I. Runs arduino-cli against every example that +# declares Arduino libraries, ensuring sketches still compile after +# upstream library bumps. Slow (~10 min) and external-dependent +# (arduino-cli + cores + libs from the index), so it's kept OUT of +# the PR workflow. +# +# Runs on a 5 AM UTC cron + on-demand via workflow_dispatch. + +on: + schedule: + - cron: '0 5 * * *' + workflow_dispatch: + +jobs: + compile: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup arduino-cli + uses: arduino/setup-arduino-cli@v2 + with: + version: 1.0.4 + + # Cache the Arduino core downloads (~500 MB total for avr + esp32 + + # rp2040). Invalidate when the upstream `arduino-cli core list` + # output changes — proxy via the lockfile alternative below. + - name: Cache Arduino cores + uses: actions/cache@v4 + with: + path: | + ~/.arduino15 + ~/Arduino + key: arduino-cores-v3-${{ runner.os }} + + - name: Update arduino-cli core index + run: | + arduino-cli config init --overwrite + arduino-cli config add board_manager.additional_urls \ + https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json \ + https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json + arduino-cli core update-index + + - name: Install board cores + run: | + arduino-cli core install arduino:avr + arduino-cli core install esp32:esp32 + arduino-cli core install rp2040:rp2040 + + - name: Install frontend dependencies + run: cd frontend && npm install --no-audit --no-fund --include=optional + + - name: Compile every example that declares libraries + run: cd frontend && npm run test:libraries + env: + RUN_LIBRARY_TESTS: '1' + + - name: Upload arduino-cli logs (on failure) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: arduino-cli-logs + path: /tmp/arduino-cli-*.log + if-no-files-found: ignore diff --git a/frontend/package.json b/frontend/package.json index 5a16d322..6b2c7570 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,9 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "test:ui": "vitest --ui" + "test:ui": "vitest --ui", + "tsc": "tsc -b", + "test:libraries": "RUN_LIBRARY_TESTS=1 vitest run src/__tests__/library-compile.integration.test.ts" }, "dependencies": { "@google/genai": "^0.6.0", @@ -75,4 +77,4 @@ "vite": "^7.3.1", "vitest": "^4.0.18" } -} +} \ No newline at end of file diff --git a/frontend/src/__tests__/library-compile.integration.test.ts b/frontend/src/__tests__/library-compile.integration.test.ts new file mode 100644 index 00000000..35af1ee2 --- /dev/null +++ b/frontend/src/__tests__/library-compile.integration.test.ts @@ -0,0 +1,126 @@ +/** + * Library compile integration test (Phase 1d-tests I — nightly). + * + * For every gallery example that declares external Arduino + * `libraries`, install them via arduino-cli + compile the example's + * sketch. Detects: + * • Library API drift in upstream releases (a new Adafruit GFX + * major breaks every dependent sketch). + * • Missing libraries in the index. + * • Sketch syntax regressions across our codebase. + * + * Gated behind `RUN_LIBRARY_TESTS=1` so the default `npm test` skips + * the entire file — arduino-cli isn't usually available in dev + * environments and the compile loop takes ~10 min. The nightly + * workflow `.github/workflows/library-compile.yml` sets the env var. + * + * Fidelity rule: imports the example arrays from the real source-of- + * truth modules. Adding a new example with `libraries` automatically + * extends this test. + */ +import { describe, it, expect } from 'vitest'; +import { execSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { analogExamples } from '../data/examples-analog'; +import { digitalExamples } from '../data/examples-digital'; +import { hundredDaysExamples } from '../data/examples-100-days'; +import { epaperExamples } from '../data/examples-displays-epaper'; +import { picowWifiExamples } from '../data/examples-picow-wifi'; +import { circuitExamples } from '../data/examples-circuits'; +import type { ExampleProject } from '../data/examples'; +import { BOARD_KIND_FQBN, type BoardKind } from '../types/board'; + +const RUN_LIBRARY_TESTS = process.env.RUN_LIBRARY_TESTS === '1'; + +const ALL_EXAMPLES: ExampleProject[] = [ + ...analogExamples, + ...digitalExamples, + ...hundredDaysExamples, + ...epaperExamples, + ...picowWifiExamples, + ...circuitExamples, +]; + +interface CompilableExample { + example: ExampleProject; + fqbn: string; + libraries: string[]; +} + +/** + * Filter to examples that: + * • Have a `code` field (Arduino sketch) + * • Have a non-empty `libraries` array (otherwise no value-add over + * a vanilla compile that already runs in dev) + * • Map to a board with a known FQBN (no Pi 3B — different toolchain) + */ +function eligibleExamples(): CompilableExample[] { + const out: CompilableExample[] = []; + for (const ex of ALL_EXAMPLES) { + if (!ex.code) continue; + if (!ex.libraries || ex.libraries.length === 0) continue; + const boardKind: BoardKind = (ex.boardType ?? 'arduino-uno') as BoardKind; + const fqbn = BOARD_KIND_FQBN[boardKind]; + if (!fqbn) continue; + out.push({ example: ex, fqbn, libraries: ex.libraries }); + } + return out; +} + +const COMPILABLE = eligibleExamples(); + +describe.skipIf(!RUN_LIBRARY_TESTS)( + `arduino-cli library compile (${COMPILABLE.length} examples)`, + () => { + it.each(COMPILABLE.map((c) => [c.example.id, c] as const))( + '%s compiles with its declared libraries', + { timeout: 300_000 }, + (_id, compilable) => { + const { example, fqbn, libraries } = compilable; + // Install libs (idempotent — arduino-cli skips already-installed). + for (const lib of libraries) { + try { + execSync(`arduino-cli lib install "${lib}"`, { + stdio: 'pipe', + encoding: 'utf8', + timeout: 60_000, + }); + } catch (err) { + throw new Error( + `[${example.id}] arduino-cli lib install "${lib}" failed: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + } + } + // Sketch dir + .ino file. + const dir = mkdtempSync(path.join(tmpdir(), `velxio-${example.id}-`)); + const inoPath = path.join(dir, `${path.basename(dir)}.ino`); + writeFileSync(inoPath, example.code ?? '', 'utf8'); + try { + execSync(`arduino-cli compile --fqbn ${fqbn} "${dir}"`, { + stdio: 'pipe', + encoding: 'utf8', + timeout: 240_000, + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error(`[${example.id}] arduino-cli compile failed:\n${msg}`); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + expect(true).toBe(true); + }, + ); + }, +); + +// Placeholder when disabled so the test runner shows "skipped" rather +// than "no tests". +describe.skipIf(RUN_LIBRARY_TESTS)('library-compile (set RUN_LIBRARY_TESTS=1 to enable)', () => { + it('placeholder', () => { + expect(RUN_LIBRARY_TESTS).toBe(false); + }); +});