schemeta/tests/compile.test.js

367 lines
11 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { compile } from "../src/compile.js";
import fixture from "../examples/esp32-audio.json" with { type: "json" };
test("compile returns svg and topology for valid model", () => {
const result = compile(fixture);
assert.equal(result.ok, true);
assert.ok(result.svg.includes("<svg"));
assert.ok(result.topology.power_domains.includes("3V3"));
assert.ok(result.topology.clock_sources.includes("U1"));
assert.ok(result.layout_metrics);
assert.equal(result.layout_metrics.overlap_edges, 0);
assert.equal(result.layout_metrics.crossings, 0);
assert.equal(typeof result.layout_metrics.total_bends, "number");
assert.equal(typeof result.layout_metrics.detour_ratio, "number");
assert.equal(typeof result.layout_metrics.label_tie_fallbacks, "number");
assert.equal(typeof result.layout_metrics.label_tie_routes, "number");
assert.ok(Array.isArray(result.bus_groups));
assert.ok(result.render_mode_used);
});
test("compile fails on invalid model", () => {
const bad = { symbols: {}, instances: [], nets: [] };
const result = compile(bad);
assert.equal(result.ok, false);
assert.ok(result.errors.length > 0);
assert.equal(result.layout_metrics.total_bends, 0);
assert.equal(result.layout_metrics.detour_ratio, 1);
assert.equal(result.layout_metrics.label_tie_routes, 0);
});
test("compile accepts render mode options", () => {
const result = compile(fixture, { render_mode: "explicit" });
assert.equal(result.ok, true);
assert.equal(result.render_mode_used, "explicit");
});
test("compile auto-creates generic symbols for unknown instances", () => {
const model = {
meta: { title: "Generic Demo" },
symbols: {},
instances: [
{
ref: "X1",
symbol: "mystery_block",
properties: { value: "Mystery" },
placement: { x: null, y: null, rotation: 0, locked: false }
},
{
ref: "X2",
symbol: "mystery_sensor",
properties: { value: "Sensor" },
placement: { x: null, y: null, rotation: 0, locked: false }
}
],
nets: [
{
name: "SIG_A",
class: "signal",
nodes: [
{ ref: "X1", pin: "IN" },
{ ref: "X2", pin: "OUT" }
]
},
{
name: "GND",
class: "ground",
nodes: [
{ ref: "X1", pin: "GND" },
{ ref: "X2", pin: "GND" }
]
}
],
constraints: {},
annotations: []
};
const result = compile(model);
assert.equal(result.ok, true);
assert.ok(result.svg.includes("<svg"));
assert.ok(result.warnings.some((w) => w.code === "auto_generic_symbol_created"));
});
test("compile auto-creates passive templates for common refs", () => {
const model = {
meta: { title: "Passive Template Demo" },
symbols: {},
instances: [
{
ref: "R1",
symbol: "resistor_generic",
properties: { value: "10k resistor" },
placement: { x: null, y: null, rotation: 0, locked: false }
},
{
ref: "U1",
symbol: "mystery_logic",
properties: { value: "Logic" },
placement: { x: null, y: null, rotation: 0, locked: false }
}
],
nets: [
{
name: "SIG",
class: "signal",
nodes: [
{ ref: "R1", pin: "1" },
{ ref: "U1", pin: "IN" }
]
},
{
name: "GND",
class: "ground",
nodes: [
{ ref: "R1", pin: "2" },
{ ref: "U1", pin: "GND" }
]
}
],
constraints: {},
annotations: []
};
const result = compile(model);
assert.equal(result.ok, true);
assert.ok(result.warnings.some((w) => w.code === "auto_template_symbol_created"));
});
test("compile accepts minimal shorthand symbols and hydrates fields", () => {
const model = {
meta: { title: "Shorthand Symbols" },
symbols: {
resistor_short: {
template_name: "resistor"
},
generic_short: {
category: "generic"
}
},
instances: [
{
ref: "R1",
symbol: "resistor_short",
properties: { value: "1k" },
placement: { x: null, y: null, rotation: 0, locked: false }
},
{
ref: "X1",
symbol: "generic_short",
properties: { value: "Mystery" },
placement: { x: null, y: null, rotation: 0, locked: false }
}
],
nets: [
{
name: "SIG",
class: "signal",
nodes: [
{ ref: "R1", pin: "1" },
{ ref: "X1", pin: "IO" }
]
},
{
name: "GND",
class: "ground",
nodes: [
{ ref: "R1", pin: "2" },
{ ref: "X1", pin: "GND" }
]
}
],
constraints: {},
annotations: []
};
const result = compile(model);
assert.equal(result.ok, true);
assert.ok(result.svg.includes("<svg"));
assert.ok(result.warnings.some((w) => w.code === "auto_template_symbol_hydrated"));
assert.ok(result.warnings.some((w) => w.code === "auto_generic_symbol_hydrated"));
});
test("compile supports instance.part without explicit symbols", () => {
const model = {
meta: { title: "Part Shortcut" },
symbols: {},
instances: [
{
ref: "R1",
part: "resistor",
properties: { value: "10k" },
placement: { x: null, y: null, rotation: 0, locked: false }
},
{
ref: "C1",
part: "capacitor",
properties: { value: "100nF" },
placement: { x: null, y: null, rotation: 0, locked: false }
}
],
nets: [
{
name: "SIG",
class: "signal",
nodes: [
{ ref: "R1", pin: "1" },
{ ref: "C1", pin: "1" }
]
},
{
name: "GND",
class: "ground",
nodes: [
{ ref: "R1", pin: "2" },
{ ref: "C1", pin: "2" }
]
}
],
constraints: {},
annotations: []
};
const result = compile(model);
assert.equal(result.ok, true);
assert.ok(result.svg.includes("<svg"));
assert.equal(result.errors.length, 0);
});
test("grouped auto-layout avoids tall single-column collapse", () => {
const model = {
meta: { title: "Group packing" },
symbols: {
q: {
symbol_id: "q",
category: "analog",
body: { width: 90, height: 70 },
pins: [
{ name: "B", number: "1", side: "left", offset: 35, type: "analog" },
{ name: "C", number: "2", side: "top", offset: 45, type: "analog" },
{ name: "E", number: "3", side: "bottom", offset: 45, type: "analog" }
]
},
adc: {
symbol_id: "adc",
category: "generic",
body: { width: 120, height: 60 },
pins: [
{ name: "IN", number: "1", side: "left", offset: 30, type: "analog" },
{ name: "3V3", number: "2", side: "top", offset: 30, type: "power_in" },
{ name: "GND", number: "3", side: "bottom", offset: 30, type: "ground" }
]
}
},
instances: [
{ ref: "Q1", symbol: "q", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "R1", part: "resistor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "R2", part: "resistor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "C1", part: "capacitor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "U1", symbol: "adc", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "R3", part: "resistor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "R4", part: "resistor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "C2", part: "capacitor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } }
],
nets: [
{ name: "N1", class: "analog", nodes: [{ ref: "Q1", pin: "C" }, { ref: "U1", pin: "IN" }] },
{ name: "N2", class: "ground", nodes: [{ ref: "Q1", pin: "E" }, { ref: "U1", pin: "GND" }] },
{ name: "N3", class: "power", nodes: [{ ref: "R1", pin: "1" }, { ref: "U1", pin: "3V3" }] }
],
constraints: {
groups: [
{ name: "front", members: ["Q1", "R1", "R2", "C1"], layout: "cluster" },
{ name: "adc", members: ["U1", "R3", "R4", "C2"], layout: "cluster" }
]
},
annotations: []
};
const result = compile(model);
assert.equal(result.ok, true);
const xs = result.layout.placed.map((p) => p.x);
const ys = result.layout.placed.map((p) => p.y);
const widthSpread = Math.max(...xs) - Math.min(...xs);
const heightSpread = Math.max(...ys) - Math.min(...ys);
assert.ok(widthSpread > 500);
assert.ok(heightSpread < 900);
});
test("multi-node signal nets render explicit junction dots", () => {
const model = {
meta: { title: "Junction coverage" },
symbols: {},
instances: [
{ ref: "R1", part: "resistor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "R2", part: "resistor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "R3", part: "resistor", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } }
],
nets: [
{
name: "SIG",
class: "signal",
nodes: [
{ ref: "R1", pin: "1" },
{ ref: "R2", pin: "1" },
{ ref: "R3", pin: "1" }
]
},
{
name: "GND",
class: "ground",
nodes: [
{ ref: "R1", pin: "2" },
{ ref: "R2", pin: "2" },
{ ref: "R3", pin: "2" }
]
}
],
constraints: {},
annotations: []
};
const result = compile(model, { render_mode: "explicit" });
assert.equal(result.ok, true);
assert.ok(result.svg.includes('data-net-junction="SIG"'));
});
test("auto-rotation chooses non-zero orientation when it improves pin alignment", () => {
const model = {
meta: { title: "Rotation heuristic" },
symbols: {
n1: {
symbol_id: "n1",
category: "generic",
body: { width: 120, height: 80 },
pins: [
{ name: "L", number: "1", side: "left", offset: 40, type: "passive" },
{ name: "R", number: "2", side: "right", offset: 40, type: "passive" }
]
},
n2: {
symbol_id: "n2",
category: "generic",
body: { width: 120, height: 80 },
pins: [
{ name: "T", number: "1", side: "top", offset: 60, type: "passive" },
{ name: "B", number: "2", side: "bottom", offset: 60, type: "passive" }
]
}
},
instances: [
{ ref: "A1", symbol: "n1", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } },
{ ref: "A2", symbol: "n2", properties: {}, placement: { x: null, y: null, rotation: 0, locked: false } }
],
nets: [
{ name: "N1", class: "signal", nodes: [{ ref: "A1", pin: "R" }, { ref: "A2", pin: "T" }] },
{ name: "N2", class: "ground", nodes: [{ ref: "A1", pin: "L" }, { ref: "A2", pin: "B" }] }
],
constraints: {},
annotations: []
};
const result = compile(model, { render_mode: "explicit" });
assert.equal(result.ok, true);
assert.ok(result.layout.placed.some((p) => (p.rotation ?? 0) % 360 !== 0));
});