"""Partial PCB: footprints with few tracks still parse and run MODE=pcb checks.""" from __future__ import annotations from pathlib import Path from backend.periscopex.models import ( Component, ComponentType, DesignGraph, Net, NetType, ) from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb from backend.periscopex.pcb_checks import run_pcb_checks from backend.periscopex.pcb_net_match import check_pcb_net_match from backend.periscopex.placement_check import check_placement _PARTIAL = """(kicad_pcb (version 20240108) (generator pcbnew) (net 0 "") (net 1 "GND") (net 2 "+3V3") (net 3 "USB_DP") (footprint "Package_SO:SOIC-8" (layer "F.Cu") (at 20 10 0) (property "Reference" "U1" (at 0 0 0) (effects (font (size 1 1)))) (pad "1" smd rect (at -2 1) (size 1 0.6) (layers "F.Cu") (net 2 "+3V3")) (pad "2" smd rect (at -2 -1) (size 1 0.6) (layers "F.Cu") (net 1 "GND")) (pad "3" smd rect (at 2 1) (size 1 0.6) (layers "F.Cu") (net 3 "USB_DP")) ) (footprint "Capacitor_SMD:C_0603" (layer "F.Cu") (at 40 30 0) (property "Reference" "C1" (at 0 0 0) (effects (font (size 1 1)))) (pad "1" smd rect (at -0.5 0) (size 0.8 0.9) (layers "F.Cu") (net 2 "+3V3")) (pad "2" smd rect (at 0.5 0) (size 0.8 0.9) (layers "F.Cu") (net 1 "GND")) ) (segment (start 18 11) (end 19 11) (width 0.2) (layer "F.Cu") (net 2)) ) """ def test_parse_footprints_with_few_tracks(tmp_path: Path): p = tmp_path / "partial.kicad_pcb" p.write_text(_PARTIAL) g = parse_kicad_pcb(p) assert "U1" in g.footprints and "C1" in g.footprints assert len(g.segments) == 1 assert g.segments[0].net == "+3V3" assert len(g.vias) == 0 def _partial_graph() -> DesignGraph: return DesignGraph( components={ "U1": Component( reference="U1", value="MCU", footprint="", component_type=ComponentType.IC, mpn="X", pins={"1": "+3V3", "2": "GND", "3": "USB_DP"}, ), "C1": Component( reference="C1", value="100n", footprint="", component_type=ComponentType.CAPACITOR, mpn="C", pins={"1": "+3V3", "2": "GND"}, ), "R9": Component( reference="R9", value="10k", footprint="", component_type=ComponentType.RESISTOR, mpn="R", pins={"1": "NRESET"}, ), }, nets={ "+3V3": Net(name="+3V3", net_type=NetType.POWER, pins=[]), "GND": Net(name="GND", net_type=NetType.GROUND, pins=[]), "USB_DP": Net(name="USB_DP", net_type=NetType.SIGNAL, pins=[]), "NRESET": Net(name="NRESET", net_type=NetType.SIGNAL, pins=[]), }, ) def test_run_pcb_checks_on_partial_layout_does_not_invent_tracks(tmp_path: Path): p = tmp_path / "partial.kicad_pcb" p.write_text(_PARTIAL) layout = parse_kicad_pcb(p) graph = _partial_graph() findings = run_pcb_checks(graph, {}, layout) lay = [f for f in findings if f.rule_id == "PE-LAY-004"] assert lay, findings assert lay[0].evidence_status == "INSUFFICIENT" assert lay[0].status == "INFO" assert all(f.rule_id != "PE-PLC-001" for f in findings) unplaced = [f for f in findings if f.rule_id == "PE-LAY-002"] assert any(f.designator == "R9" for f in unplaced) def test_partial_board_via_is_not_a_pad(tmp_path: Path): from tests.test_pcb_via_not_pad import _qfn24_pcb layout = parse_kicad_pcb(_qfn24_pcb(tmp_path, board_vias=[(80.0, 80.0, "GND", 1)])) u1 = layout.footprints["U1"] assert len(u1.pads) == 24 assert len(layout.vias) == 1 via = layout.vias[0] assert all(abs(p.x - via.x) > 0.01 or abs(p.y - via.y) > 0.01 for p in u1.pads) def test_unplaced_ref_is_lay_002_not_invented_xy(): graph = _partial_graph() from backend.periscopex.models import LayoutFootprint, LayoutGraph, LayoutPad layout = LayoutGraph( footprints={ "U1": LayoutFootprint( reference="U1", x=0, y=0, layer="F.Cu", pads=[LayoutPad(number="1", x=0, y=0, net="+3V3")], ), }, ) findings = check_pcb_net_match(graph, layout) assert any(f.rule_id == "PE-LAY-002" and f.designator == "C1" for f in findings) assert check_placement(graph, {}, layout) == [] or all( f.evidence_status == "INSUFFICIENT" or f.rule_id != "PE-PLC-001" for f in check_placement(graph, {}, layout) )