import { describe, expect, it } from "vitest"; import { LIBRARY_FILES, METHODS, RR_METHOD, SCENARIO_PRESETS, buildPlanBundle, buildRunBundle, formatNumber, generateParamsBlock, isPlainStruct, modesKey, parseMethodPlan, validateSpec, withEdits, } from "../src/index"; import { runner, testSpec } from "./helpers"; describe("source generation and editing", () => { it("formats numbers as exact, readable MATLAB literals", () => { expect(formatNumber(200e9)).toBe("200e9"); expect(formatNumber(0.002)).toBe("0.002"); expect(formatNumber(96000)).toBe("96000"); expect(formatNumber(1e-3)).toBe("0.001"); expect(formatNumber(2.5e-7)).toBe("250e-9"); expect(Number(formatNumber(68.9e9))).toBe(68.9e9); expect(formatNumber(-3)).toBe("-3"); }); it("round-trips the spec through the params block", async () => { const spec = testSpec(); const block = generateParamsBlock(spec); const run = await runner.run({ bundle: { files: [{ name: "p.m", source: block }, ...LIBRARY_FILES], entry: "p.m", resultVariable: "params", }, }); expect(run.ok).toBe(true); if (!run.ok || !isPlainStruct(run.value)) return; const plate = run.value.fields.plate; const mat = run.value.fields.material; const hammer = run.value.fields.hammer; if (!isPlainStruct(plate) || !isPlainStruct(mat) || !isPlainStruct(hammer)) throw new Error("params fields are not structs"); expect(plate.fields.thickness).toBe(spec.plate.thickness); expect(mat.fields.E).toBe(spec.material.E); expect(mat.fields.name).toBe(spec.material.name); expect(hammer.fields.restitution).toBe(spec.hammer.restitution); }); it("plans every scenario with both methods", async () => { for (const sc of SCENARIO_PRESETS) for (const m of METHODS) { const run = await runner.run({ bundle: buildPlanBundle(sc.spec, m) }); expect(run.ok, `${sc.id}/${m.id}: ${run.ok ? "" : run.error}`).toBe(true); if (!run.ok) continue; const plan = parseMethodPlan(run.value); expect(plan.method).toBe(m.id); expect(plan.modes_expected).toBeGreaterThan(0); expect(plan.pendulum_hz).toBeGreaterThan(0); expect(plan.hertz_duration).toBeGreaterThan(0); expect(plan.work).toBeGreaterThan(0); } }); it("runs the edited source, and surfaces numbl errors with file and line", async () => { const spec = testSpec(); const bundle = buildRunBundle(spec, RR_METHOD); const main = bundle.files.find((f) => f.name === "main.m")!.source; const edited = withEdits(bundle, { "main.m": main.replace( "modes = plate_rr_modes(params);", "modes = undefined_function_xyz(params);", ), }); const run = await runner.run({ bundle: edited }); expect(run.ok).toBe(false); if (run.ok) return; expect(run.error).toMatch(/undefined_function_xyz/); expect(run.error).toMatch(/main\.m/); }); it("reports a missing result variable rather than an empty screen", async () => { const spec = testSpec(); const bundle = buildRunBundle(spec, RR_METHOD); const edited = withEdits(bundle, { "main.m": "x = 1;" }); const run = await runner.run({ bundle: edited }); expect(run.ok).toBe(false); if (!run.ok) expect(run.error).toMatch(/result/); }); it("validates obviously bad specs, and accepts every scenario", () => { const spec = testSpec(); spec.probe.x = 10; spec.hammer.restitution = 1.5; spec.hole.edge_offset = 0.1; const issues = validateSpec(spec); expect(issues.some((i) => i.path === "probe" && i.severity === "error")).toBe(true); expect(issues.some((i) => i.path === "hammer.restitution")).toBe(true); expect(issues.some((i) => i.path === "hole.edge_offset")).toBe(true); for (const sc of SCENARIO_PRESETS) expect( validateSpec(sc.spec).filter((i) => i.severity === "error"), sc.id, ).toEqual([]); }); it("keys the modes on what they depend on, and nothing else", () => { const a = testSpec(); const b = testSpec(); b.strike.x = 0.005; b.hammer.mass = 0.02; b.material.Q = 500; b.thread.dashpot = 0; b.mic.distance = 1; b.render.duration = 1; expect(modesKey(a, "rr")).toBe(modesKey(b, "rr")); expect(modesKey(a, "fem")).toBe(modesKey(b, "fem")); const c = testSpec(); c.plate.thickness = 0.002; expect(modesKey(a, "rr")).not.toBe(modesKey(c, "rr")); const d = testSpec(); d.fem.mesh_x = 32; expect(modesKey(a, "rr")).toBe(modesKey(d, "rr")); expect(modesKey(a, "fem")).not.toBe(modesKey(d, "fem")); const e = testSpec(); e.render.fs = 48000; expect(modesKey(a, "rr")).not.toBe(modesKey(e, "rr")); }); });