Replace findings list with a tree; filter ESD/PI false positives.
PE-ESD-001 only on J* connector–IC nets (skip NC, unconnected, VSYS/GND/3V3). PE-PI-001 treats any capacitor on the rail, including KiCad slash prefixes, as decoupling. Sort findings ERROR then WARNING then INFO, then RULE/RISK/REVIEW/INFO. Report sidebar is a collapsed expand-on-click tree instead of an all-open list. GET /report and complete_findings fail-soft and sort the same way.
This commit is contained in:
@@ -306,3 +306,190 @@ def test_llm_error_review_clamped_in_complete_finding():
|
||||
complete_finding(f)
|
||||
assert f.finding_class == "REVIEW"
|
||||
assert f.status == "WARNING"
|
||||
|
||||
|
||||
def test_esd_skips_gpio_nc_unconnected_and_onboard_power():
|
||||
graph = DesignGraph(
|
||||
components={
|
||||
"J1": Component(
|
||||
reference="J1", value="HDR", footprint="",
|
||||
component_type=ComponentType.CONNECTOR, mpn="",
|
||||
pins={
|
||||
"1": "unconnected-J1-Pad1",
|
||||
"2": "NC",
|
||||
"3": "VSYS",
|
||||
"4": "GND",
|
||||
"5": "3V3",
|
||||
"6": "USB_DP",
|
||||
},
|
||||
),
|
||||
"U1": Component(
|
||||
reference="U1", value="MCU", footprint="",
|
||||
component_type=ComponentType.IC, mpn="MCU",
|
||||
pins={
|
||||
"1": "GPIO9",
|
||||
"2": "LED_SDATA",
|
||||
"3": "VSYS",
|
||||
"4": "GND",
|
||||
"5": "3V3",
|
||||
"6": "USB_DP",
|
||||
},
|
||||
),
|
||||
},
|
||||
nets={},
|
||||
)
|
||||
findings = check_esd(graph, {})
|
||||
nets = {f.net for f in findings}
|
||||
assert nets == {"USB_DP"}
|
||||
assert "unused" not in (findings[0].recommendation or "").lower()
|
||||
assert len(findings) == 1
|
||||
|
||||
|
||||
def test_esd_one_finding_per_connector_ic_path():
|
||||
graph = DesignGraph(
|
||||
components={
|
||||
"J1": Component(
|
||||
reference="J1", value="USB", footprint="",
|
||||
component_type=ComponentType.CONNECTOR, mpn="",
|
||||
pins={"1": "USB_DP"},
|
||||
),
|
||||
"U2": Component(
|
||||
reference="U2", value="A", footprint="",
|
||||
component_type=ComponentType.IC, mpn="A",
|
||||
pins={"1": "USB_DP"},
|
||||
),
|
||||
"U3": Component(
|
||||
reference="U3", value="B", footprint="",
|
||||
component_type=ComponentType.IC, mpn="B",
|
||||
pins={"1": "USB_DP"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"USB_DP": Net(
|
||||
name="USB_DP", net_type=NetType.SIGNAL,
|
||||
pins=[
|
||||
PinConnection(component_ref="J1", pin_number="1"),
|
||||
PinConnection(component_ref="U2", pin_number="1"),
|
||||
PinConnection(component_ref="U3", pin_number="1"),
|
||||
],
|
||||
),
|
||||
},
|
||||
)
|
||||
findings = check_esd(graph, {})
|
||||
assert len(findings) == 2
|
||||
assert {f.designator for f in findings} == {"U2", "U3"}
|
||||
|
||||
|
||||
def test_esd_skips_when_protection_part_on_net():
|
||||
graph = DesignGraph(
|
||||
components={
|
||||
"J1": Component(
|
||||
reference="J1", value="USB", footprint="",
|
||||
component_type=ComponentType.CONNECTOR, mpn="",
|
||||
pins={"1": "USB_DP"},
|
||||
),
|
||||
"U2": Component(
|
||||
reference="U2", value="UART", footprint="",
|
||||
component_type=ComponentType.IC, mpn="CH",
|
||||
pins={"1": "USB_DP"},
|
||||
),
|
||||
"D1": Component(
|
||||
reference="D1", value="USBLC6 ESD", footprint="",
|
||||
component_type=ComponentType.DISCRETE, mpn="",
|
||||
pins={"1": "USB_DP"},
|
||||
),
|
||||
},
|
||||
nets={},
|
||||
)
|
||||
assert check_esd(graph, {}) == []
|
||||
|
||||
|
||||
def test_pi_any_cap_on_slash_prefixed_rail_suppresses_missing_local():
|
||||
cons = ComponentConstraints(
|
||||
mpn="LDO",
|
||||
pintable=[Pin(number="1", name="VIN")],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
)
|
||||
graph = DesignGraph(
|
||||
components={
|
||||
"U1": Component(
|
||||
reference="U1", value="LDO", footprint="",
|
||||
component_type=ComponentType.IC, mpn="LDO",
|
||||
pins={"1": "VBUS"},
|
||||
),
|
||||
"C68": Component(
|
||||
reference="C68", value="10u", footprint="",
|
||||
component_type=ComponentType.CAPACITOR, mpn="",
|
||||
pins={"1": "/VBUS", "2": "GND"},
|
||||
specs=CapacitorSpecs(value_farads=10e-6, value_formatted="10u"),
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"VBUS": Net(
|
||||
name="VBUS", net_type=NetType.POWER, voltage=5.0,
|
||||
pins=[PinConnection(component_ref="U1", pin_number="1")],
|
||||
),
|
||||
"/VBUS": Net(
|
||||
name="/VBUS", net_type=NetType.POWER, voltage=5.0,
|
||||
pins=[PinConnection(component_ref="C68", pin_number="1")],
|
||||
),
|
||||
},
|
||||
)
|
||||
cons.pintable = [Pin(number="1", name="VBUS")]
|
||||
findings = check_power_integrity(graph, {"LDO": cons})
|
||||
assert [f.rule_id for f in findings if f.rule_id == "PE-PI-001"] == []
|
||||
|
||||
|
||||
def test_pi_bulk_only_does_not_claim_no_local():
|
||||
cons = ComponentConstraints(
|
||||
mpn="LDO",
|
||||
pintable=[Pin(number="1", name="VIN")],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
)
|
||||
graph = DesignGraph(
|
||||
components={
|
||||
"U1": Component(
|
||||
reference="U1", value="LDO", footprint="",
|
||||
component_type=ComponentType.IC, mpn="LDO",
|
||||
pins={"1": "VSYS"},
|
||||
),
|
||||
"C71": Component(
|
||||
reference="C71", value="10u", footprint="",
|
||||
component_type=ComponentType.CAPACITOR, mpn="",
|
||||
pins={"1": "VSYS", "2": "GND"},
|
||||
specs=CapacitorSpecs(value_farads=10e-6, value_formatted="10u"),
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"VSYS": Net(
|
||||
name="VSYS", net_type=NetType.POWER, voltage=5.0,
|
||||
pins=[
|
||||
PinConnection(component_ref="U1", pin_number="1"),
|
||||
PinConnection(component_ref="C71", pin_number="1"),
|
||||
],
|
||||
),
|
||||
},
|
||||
)
|
||||
findings = check_power_integrity(graph, {"LDO": cons})
|
||||
assert [f.rule_id for f in findings if f.rule_id == "PE-PI-001"] == []
|
||||
|
||||
|
||||
def test_sort_findings_error_then_class():
|
||||
from backend.periscopex.finding_engine import sort_findings
|
||||
|
||||
items = [
|
||||
Finding(designator="U1", finding="info review", why="x", status="INFO", finding_class="REVIEW"),
|
||||
Finding(designator="U1", finding="warn risk", why="x", status="WARNING", finding_class="RISK"),
|
||||
Finding(designator="U1", finding="err rule", why="x", status="ERROR", finding_class="RULE"),
|
||||
Finding(designator="U1", finding="warn review", why="x", status="WARNING", finding_class="REVIEW"),
|
||||
]
|
||||
sort_findings(items)
|
||||
assert [f.finding for f in items] == [
|
||||
"err rule",
|
||||
"warn risk",
|
||||
"warn review",
|
||||
"info review",
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user