CI / Tool tests and sync-check (push) Successful in 13s
CI / Verify references (all modules, Rust and TS) (push) Successful in 1m32s
CI / Verify Expansion Drop solutions (all modules, Rust and TS) (push) Successful in 1m35s
CI / Coupling audit on references (push) Successful in 13s
CI / Site build (push) Successful in 25s
CI / Naive engine tests (Module 0) (push) Successful in 32s
- verify-references --expansion overlays expansion/solution on each reference and runs the cumulative tests plus the drop test; refuses a solution that touches src/engine/ - CI job verify-expansions - README: workspace-template + git init step, tool list, repo disclaimer link - Cargo.lock for every Rust reference (staged with the reference, pins serde for rustc 1.85.1) Co-Authored-By: Claude Opus 5.5 <[email protected]>
122 lines
5.4 KiB
JavaScript
122 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// For each module <= N with a reference/: copy reference + tests of modules 01..module into a temp dir, run tests.
|
|
// The card data (loaded by the references from Module 5) is staged as <dir>/cards: course/cards
|
|
// from Module 5 on (Modules 1-4 keep their cards in code), plus the cards/ of each module up to
|
|
// the one verified. Each tested module's levels/ (the puzzle levels its tests load, from
|
|
// Module 10) is staged as <dir>/levels.
|
|
//
|
|
// node tools/verify-references.mjs [NN] [--expansion]
|
|
//
|
|
// --expansion checks each module's Expansion Drop solution instead: the reference is staged as
|
|
// above, then the module's expansion/ is overlaid (see overlayExpansion) and the cumulative tests
|
|
// run together with the drop's own test. Modules without an expansion/solution are skipped.
|
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
import { spawnSync } from "node:child_process";
|
|
import { tmpdir } from "node:os";
|
|
import { basename, join, relative } from "node:path";
|
|
import { cardDirs, moduleDirs, testFiles, isMain } from "./lib.mjs";
|
|
|
|
// A staged reference has no node_modules, so TS installs the reference's own
|
|
// pinned devDependencies first (a bare `npx vitest` would fetch an unrelated version).
|
|
const DEFAULTS = {
|
|
rust: "cargo test",
|
|
ts: "npm install --no-audit --no-fund --loglevel=error && npx vitest run",
|
|
};
|
|
|
|
export function commandFor(lang, env = process.env) {
|
|
return env[`VERIFY_CMD_${lang.toUpperCase()}`] || DEFAULTS[lang];
|
|
}
|
|
|
|
export function planVerification(mods, upTo) {
|
|
return mods
|
|
.filter((m) => Number(m.num) >= 1 && Number(m.num) <= upTo)
|
|
.map((m) => ({
|
|
...m,
|
|
langs: ["rust", "ts"].filter((l) => existsSync(join(m.path, "reference", l))),
|
|
testModules: mods.filter((t) => Number(t.num) >= 1 && Number(t.num) <= Number(m.num)),
|
|
}))
|
|
.filter((p) => p.langs.length > 0);
|
|
}
|
|
|
|
/** Files under `dir`, as paths relative to it ("/"-separated). */
|
|
function files(dir, root = dir, out = []) {
|
|
for (const e of readdirSync(dir)) {
|
|
const p = join(dir, e);
|
|
statSync(p).isDirectory() ? files(p, root, out) : out.push(relative(root, p).split("\\").join("/"));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Does this module's Expansion Drop have a solution to check for `lang`? */
|
|
export function hasExpansion(plan, lang) {
|
|
const exp = join(plan.path, "expansion");
|
|
return existsSync(join(exp, "solution")) && testFiles(exp, lang).length > 0;
|
|
}
|
|
|
|
/**
|
|
* Overlay a module's Expansion Drop solution onto a staged reference, as a learner applies it:
|
|
* expansion/solution/<lang>/ over the reference root, solution/cards/ into cards/,
|
|
* solution/words.json and solution/seams.json into src/content/, and the drop's test into tests/.
|
|
* Throws if the solution touches src/engine/ (a drop changes content only).
|
|
*/
|
|
export function overlayExpansion(plan, lang, dir) {
|
|
const sol = join(plan.path, "expansion", "solution");
|
|
const code = join(sol, lang);
|
|
if (existsSync(code)) {
|
|
const engine = files(code).filter((f) => f.startsWith("src/engine/"));
|
|
if (engine.length) throw new Error(`${plan.num} expansion solution changes the engine: ${engine.join(", ")}`);
|
|
cpSync(code, dir, { recursive: true });
|
|
}
|
|
if (existsSync(join(sol, "cards"))) cpSync(join(sol, "cards"), join(dir, "cards"), { recursive: true });
|
|
for (const f of ["words.json", "seams.json"]) {
|
|
if (existsSync(join(sol, f))) cpSync(join(sol, f), join(dir, "src", "content", f));
|
|
}
|
|
for (const f of testFiles(join(plan.path, "expansion"), lang)) cpSync(f, join(dir, "tests", basename(f)));
|
|
return dir;
|
|
}
|
|
|
|
export function stage(plan, lang) {
|
|
const dir = mkdtempSync(join(tmpdir(), `verify-${plan.num}-${lang}-`));
|
|
cpSync(join(plan.path, "reference", lang), dir, { recursive: true });
|
|
mkdirSync(join(dir, "tests"), { recursive: true });
|
|
for (const t of plan.testModules) {
|
|
for (const f of testFiles(t.path, lang)) cpSync(f, join(dir, "tests", basename(f)));
|
|
if (existsSync(join(t.path, "levels"))) cpSync(join(t.path, "levels"), join(dir, "levels"), { recursive: true });
|
|
}
|
|
for (const cards of cardDirs(plan.num)) cpSync(cards, join(dir, "cards"), { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
if (isMain(import.meta.url)) {
|
|
const args = process.argv.slice(2);
|
|
const expansion = args.includes("--expansion");
|
|
const num = args.find((a) => a !== "--expansion");
|
|
const all = moduleDirs();
|
|
const upTo = num ? Number(num) : Math.max(0, ...all.map((m) => Number(m.num)));
|
|
const plans = planVerification(all, upTo);
|
|
if (!plans.length) console.log("verify-references: no modules with reference/ up to " + upTo);
|
|
let failed = false;
|
|
for (const p of plans) {
|
|
for (const lang of p.langs) {
|
|
if (expansion && !hasExpansion(p, lang)) {
|
|
console.log(`module ${p.num}-${p.slug} [${lang}] expansion: skipped (no expansion solution)`);
|
|
continue;
|
|
}
|
|
const dir = stage(p, lang);
|
|
let r;
|
|
try {
|
|
if (expansion) overlayExpansion(p, lang, dir);
|
|
r = spawnSync(commandFor(lang), { cwd: dir, shell: true, encoding: "utf8" });
|
|
} catch (e) {
|
|
r = { status: 1, stdout: "", stderr: String(e.message) };
|
|
}
|
|
const ok = r.status === 0;
|
|
failed ||= !ok;
|
|
console.log(`module ${p.num}-${p.slug} [${lang}]${expansion ? " expansion" : ""}: ${ok ? "ok" : "FAIL"}`);
|
|
if (!ok) console.log((r.stdout || "") + (r.stderr || ""));
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
if (failed) process.exit(1);
|
|
}
|