import { describe, expect, it } from "vitest"; import { FD_METHOD, SCENARIO_PRESETS, buildRunBundle, formatNumber, generateParamsBlock, isPlainStruct, withEdits, LIBRARY_FILES, validateSpec, } 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(44100)).toBe("44100"); expect(formatNumber(1e-3)).toBe("0.001"); expect(formatNumber(2.5e-7)).toBe("250e-9"); expect(formatNumber(7850)).toBe("7850"); 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 bundle = { files: [{ name: "p.m", source: block }, ...LIBRARY_FILES], entry: "p.m", resultVariable: "params", }; const run = await runner.run({ bundle }); 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 mount = run.value.fields.mount; if (!isPlainStruct(plate) || !isPlainStruct(mat) || !isPlainStruct(mount)) throw new Error("params fields are not structs"); expect(plate.fields.h).toBe(spec.plate.h); expect(mat.fields.E).toBe(spec.material.E); expect(mat.fields.name).toBe(spec.material.name); expect(mount.fields.type).toBe("thread"); }); it("writes every mount as a struct numbl accepts", async () => { for (const sc of SCENARIO_PRESETS) { const block = generateParamsBlock(sc.spec); const run = await runner.run({ bundle: { files: [ { name: "p.m", source: block + "\nP = plate_setup(params);\n" }, ...LIBRARY_FILES, ], entry: "p.m", resultVariable: "P", }, }); expect(run.ok, `${sc.id}: ${run.ok ? "" : run.error}`).toBe(true); } }); it("runs the edited source, and surfaces numbl errors with file and line", async () => { const spec = testSpec(); const bundle = buildRunBundle(spec, FD_METHOD); const main = bundle.files.find((f) => f.name === "main.m")!.source; const edited = withEdits(bundle, { "main.m": main.replace( "result = plate_fd(params);", "result = plate_fd(params)\nresult = undefined_function_xyz(1);", ), }); 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, FD_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", () => { const spec = testSpec(); spec.probe.x = 10; spec.fd.cfl = 1.2; const issues = validateSpec(spec); expect(issues.some((i) => i.path === "probe" && i.severity === "error")).toBe(true); expect(issues.some((i) => i.path === "fd.cfl" && i.severity === "warning")).toBe(true); const two = testSpec(); two.mount = { type: "threads", x1: 0.2, y1: 0.19, x2: 0.28, y2: 0.19, length: 0.3 }; expect(validateSpec(two).some((i) => /slack/.test(i.message))).toBe(true); for (const sc of SCENARIO_PRESETS) expect( validateSpec(sc.spec).filter((i) => i.severity === "error"), sc.id, ).toEqual([]); }); });