ci: Phase 1d-tests I + K + L — workflow hardening + nightly library-compile
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) <noreply@anthropic.com>
This commit is contained in:
parent
68c19a6663
commit
07552b5d9e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue