velxio/frontend/src/__tests__/library-compile.integration...

125 lines
4.4 KiB
TypeScript

/**
* 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 { 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,
...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);
});
});