memlnaut-nisps/codegen/tests/golden_test.ts
w1n5t0n 129e28b207 Add codegen tool: schemas -> C++ headers + TS modules
bun-runnable TypeScript script (codegen/generate.ts) that:
- Validates each schemas/modes/*.json against the meta-schema via
  ajv (Draft 2020-12).
- Cross-checks params.length == ml.output_size and
  ml.input_channels.length == ml.input_size.
- Emits constexpr C++ data into nisps/modes/generated/ (one
  schema_types.hpp + one <mode_id>_schema.hpp per mode). Uses
  std::string_view + std::array; no std::vector, no heap, .f
  suffixed float literals (perf contract §3.3).
- Emits TS modules into playground/src/modes/generated/ (one
  types.ts + one <mode_id>_schema.ts + index.ts barrel).
- Idempotent: re-running yields byte-identical output.
- Exits non-zero on schema validation failure.

Reference templates live in codegen/templates/ (not consumed at
codegen time -- for human reviewers).

Golden test (codegen/tests/golden_test.ts) snapshots
paf_synth_schema.{hpp,ts} and verifies regeneration matches the
golden + that a second run is idempotent.

Until stream 1 lands nisps/core/math.hpp, schema_types.hpp ships
its own minimal Curve enum with a TODO marker pointing at the
eventual include.
2026-04-29 15:29:08 +03:00

119 lines
3.5 KiB
TypeScript

#!/usr/bin/env bun
/**
* Golden test: re-runs codegen and asserts that the freshly-generated
* paf_synth_schema.{hpp,ts} files are byte-identical to the snapshots in
* codegen/tests/golden/.
*
* If you change the codegen template intentionally, regenerate the goldens:
* bun run generate.ts
* cp ../nisps/modes/generated/paf_synth_schema.hpp tests/golden/
* cp ../playground/src/modes/generated/paf_synth_schema.ts tests/golden/
*/
import { readFileSync, existsSync } from "node:fs";
import { join, dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { main as runGenerate } from "../generate.ts";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "..", "..");
const GOLDEN_DIR = join(__dirname, "golden");
interface Case {
name: string;
generatedPath: string;
goldenPath: string;
}
const CASES: Case[] = [
{
name: "paf_synth_schema.hpp",
generatedPath: join(REPO_ROOT, "nisps", "modes", "generated", "paf_synth_schema.hpp"),
goldenPath: join(GOLDEN_DIR, "paf_synth_schema.hpp"),
},
{
name: "paf_synth_schema.ts",
generatedPath: join(REPO_ROOT, "playground", "src", "modes", "generated", "paf_synth_schema.ts"),
goldenPath: join(GOLDEN_DIR, "paf_synth_schema.ts"),
},
];
function diff(name: string, expected: string, actual: string): string {
const e = expected.split("\n");
const a = actual.split("\n");
const out: string[] = [];
out.push(`mismatch in ${name}:`);
const maxLen = Math.max(e.length, a.length);
let mismatches = 0;
for (let i = 0; i < maxLen; i++) {
if (e[i] !== a[i]) {
mismatches++;
if (mismatches <= 10) {
out.push(` L${i + 1}:`);
out.push(` expected: ${JSON.stringify(e[i] ?? "<EOF>")}`);
out.push(` actual : ${JSON.stringify(a[i] ?? "<EOF>")}`);
}
}
}
if (mismatches > 10) {
out.push(` ... and ${mismatches - 10} more line mismatches`);
}
return out.join("\n");
}
function run(): number {
console.log("running codegen…");
const code = runGenerate();
if (code !== 0) {
console.error(`codegen exited ${code}; aborting golden test`);
return code;
}
let failed = 0;
for (const c of CASES) {
if (!existsSync(c.goldenPath)) {
console.error(`MISSING golden file: ${c.goldenPath}`);
failed++;
continue;
}
if (!existsSync(c.generatedPath)) {
console.error(`MISSING generated file: ${c.generatedPath}`);
failed++;
continue;
}
const expected = readFileSync(c.goldenPath, "utf8");
const actual = readFileSync(c.generatedPath, "utf8");
if (expected !== actual) {
console.error(diff(c.name, expected, actual));
failed++;
} else {
console.log(` ok ${c.name}`);
}
}
// Idempotency check: re-run codegen, files must still match goldens.
console.log("re-running codegen for idempotency check…");
const code2 = runGenerate();
if (code2 !== 0) {
console.error(`second codegen run exited ${code2}; aborting`);
return code2;
}
for (const c of CASES) {
if (!existsSync(c.goldenPath) || !existsSync(c.generatedPath)) continue;
const expected = readFileSync(c.goldenPath, "utf8");
const actual = readFileSync(c.generatedPath, "utf8");
if (expected !== actual) {
console.error(`idempotency: ${c.name} drifted on second run`);
failed++;
}
}
if (failed > 0) {
console.error(`\n${failed} golden test(s) failed.`);
return 1;
}
console.log("\nall golden tests passed.");
return 0;
}
process.exit(run());