astra-abse/t113-s3
Allwinner T113-S3 / JLCPCB C5197687: native saved fanout, LCD top, storage right, 127 perimeter exits, 30 decoupling capacitors, connected GND planes; clean DRC and shorts, all 29 vias connected.
- Version
- 1.2.0
- License
- MIT
- Stars
- 0
tests/fanout.test.tsx
import { beforeAll, expect, test } from "bun:test";
import { Circuit } from "@tscircuit/core";
import { runAllChecks } from "@tscircuit/checks";
import type { AnyCircuitElement } from "circuit-json";
import { readFile } from "node:fs/promises";
import Board from "../index.circuit";
import { T113S3Module } from "../src/T113S3Module";
import { auditFanout, elements } from "../scripts/audit";
import { auditConnectivity } from "../scripts/connectivity";
import { checkCopperClearance } from "../scripts/copper-clearance";
let json: AnyCircuitElement[];
beforeAll(async () => {
const circuit = new Circuit();
circuit.add(<Board />);
await circuit.renderUntilSettled();
json = circuit.getCircuitJson();
}, 30000);
test("all imported pads escape to the declared boundary with clean DRC", async () => {
expect(auditFanout(json).escapes).toBe(127);
expect(auditConnectivity(json).connectedGroundPads).toBe(32);
expect(await runAllChecks(json)).toEqual([]);
expect(checkCopperClearance(json).violations).toEqual([]);
}, 30000);
test.each([0, 90, 180, 270] as const)(
"module translation and %d degree rotation preserve the fanout",
async (rotation) => {
const circuit = new Circuit();
circuit.add(
<board
width={60}
height={60}
layers={4}
minTraceWidth={0.1}
defaultTraceWidth={0.1}
minTraceToPadEdgeClearance={0.1}
minViaEdgeToPadEdgeClearance={0.1}
>
<schematicsheet name="Main" sheetWidth={550} sheetHeight={350}>
<T113S3Module
name="MOVED"
pcbX={4.25}
pcbY={-3.5}
pcbRotation={rotation}
/>
</schematicsheet>
</board>,
);
await circuit.renderUntilSettled();
const moved = circuit.getCircuitJson();
expect(auditFanout(moved, { x: 4.25, y: -3.5 }, rotation).escapes).toBe(
127,
);
expect(auditConnectivity(moved).connectedVias).toBe(29);
expect(await runAllChecks(moved)).toEqual([]);
expect(checkCopperClearance(moved).violations).toEqual([]);
},
30000,
);
test("removing an escape is detected, rather than a vacuous clean DRC", async () => {
const broken = json.filter(
(e) =>
e.type !== "pcb_trace" ||
e.pcb_trace_id !== elements(json, "pcb_trace")[0]!.pcb_trace_id,
);
expect(() => auditFanout(broken)).toThrow();
expect(
(await runAllChecks(broken)).some((e) =>
/missing|not_connected/.test(e.type),
),
).toBe(true);
}, 30000);
test("an injected bridge to a neighboring net is detected", async () => {
const broken = structuredClone(json);
const trace = elements(broken, "pcb_trace")[0]!;
// Cross the neighboring copper in the middle; do not create a new endpoint
// exactly on a port, which core may infer as an intended connection.
trace.route.push({
route_type: "wire",
x: 17,
y: -5.55,
layer: "top",
width: 0.1,
});
expect(
(await runAllChecks(broken)).some((e) =>
/trace_error|clearance/.test(e.type),
),
).toBe(true);
}, 30000);
test("a stale breakout marker cannot hide an incomplete copper endpoint", () => {
const broken = structuredClone(json);
const end = elements(broken, "pcb_trace")[0]!.route.at(-1)!;
if (end.route_type !== "wire") throw new Error("Expected endpoint");
end.x -= 1;
expect(() => auditFanout(broken)).toThrow(/copper end/);
});
test("source-based copper audit catches a bridge ending exactly on another net's pad", () => {
const broken = structuredClone(json);
const trace = elements(broken, "pcb_trace")[0]!;
const victim = elements(broken, "pcb_port")[1]!;
trace.route.push({
route_type: "wire",
x: victim.x,
y: victim.y,
layer: "top",
width: 0.1,
});
expect(checkCopperClearance(broken).violations.length).toBeGreaterThan(0);
});
test("unsupported profiles fail explicitly", () => {
// @ts-expect-error Exercise runtime validation for untyped consumers.
expect(() => T113S3Module({ name: "SOC", routingProfile: "wrong" })).toThrow(
/Unknown routing profile/,
);
});
test("the JLCPCB import is unchanged from its recorded provenance", async () => {
const { createHash } = await import("node:crypto");
const provenance = JSON.parse(
await readFile("imports/provenance.json", "utf8"),
);
const raw = await readFile("imports/T113_S3.tsx");
expect(createHash("sha256").update(raw).digest("hex")).toBe(
provenance.sha256,
);
});
test("removing the bottom ground plane disconnects the capacitor returns", () => {
const broken = json.filter(
(e) => e.type !== "pcb_copper_pour" || e.layer !== "bottom",
);
expect(() => auditConnectivity(broken)).toThrow(/ground pad disconnected/);
});
test("a missing power via breaks the physical capacitor connection", () => {
const broken = json.filter(
(e) => e.type !== "pcb_via" || e.pcb_via_id !== "pcb_via_3",
);
expect(() => auditConnectivity(broken)).toThrow(/power pad disconnected/);
});
test("a ground pour without its clearance holes is detected as a short", () => {
const broken = structuredClone(json);
const pour = elements(broken, "pcb_copper_pour").find(
(p) => p.layer === "bottom",
)!;
if (pour.shape !== "brep") throw new Error("Expected polygon pour");
pour.brep_shape.inner_rings = [];
expect(checkCopperClearance(broken).violations.length).toBeGreaterThan(0);
});