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);
});