Add /api/library datasheet import and component GET/PUT with no exam. Reorganize pytest into datasheet, library, schematic, PCB, and AF+AI. Document Rust criteria (none chosen; no rustup) and coding conformity.
429 lines
15 KiB
Python
429 lines
15 KiB
Python
"""G1 SI vs simple_project — no invented millimetres or USBPHY boards.
|
||
|
||
Favor: real /USB.D+ and /USB.D- pair by suffix; eval has no PE-SI keys.
|
||
Against: no .kicad_pcb → no PE-SI-001; 3W is not invented.
|
||
I2C/GPIO/CC are not 50/90 Ω. ImpedenceFinder numbers vs layout_rules only.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from tests.paths import SIMPLE_PROJECT, TAXONOMY
|
||
|
||
from pathlib import Path
|
||
|
||
from backend.periscopex.eval_report import eval_simple_project
|
||
from backend.periscopex.finding_engine import complete_finding
|
||
from backend.periscopex.models import (
|
||
Component,
|
||
ComponentConstraints,
|
||
ComponentType,
|
||
DesignGraph,
|
||
LayoutGraph,
|
||
LayoutSegment,
|
||
LayoutVia,
|
||
Net,
|
||
NetType,
|
||
Pin,
|
||
PinConnection,
|
||
)
|
||
from backend.periscopex.si_check import bus_class, check_si, partner_net, skip_si_net
|
||
|
||
SIMPLE = SIMPLE_PROJECT
|
||
|
||
|
||
def _graph() -> DesignGraph:
|
||
return DesignGraph.model_validate_json(
|
||
(SIMPLE / "design_graph.json").read_text()
|
||
)
|
||
|
||
|
||
def test_simple_project_usb_dp_dm_are_a_named_pair():
|
||
g = _graph()
|
||
assert "/USB.D+" in g.nets
|
||
assert "/USB.D-" in g.nets
|
||
assert partner_net("/USB.D+") == "/USB.D-"
|
||
assert partner_net("/USB.D-") == "/USB.D+"
|
||
assert partner_net("/USBC.D+") == "/USBC.D-"
|
||
|
||
|
||
def test_simple_project_without_pcb_has_no_ps_si_001():
|
||
findings = check_si(_graph(), {}, None)
|
||
assert findings == []
|
||
assert all(f.rule_id != "PE-3W-001" for f in findings)
|
||
|
||
|
||
def test_simple_project_eval_has_no_si_keys():
|
||
scores = eval_simple_project(SIMPLE)
|
||
assert scores.finding_count == 8
|
||
assert scores.precision == 1.0
|
||
assert scores.recall == 1.0
|
||
assert not any(
|
||
k.startswith("PE-SI-") or k.startswith("PE-3W-")
|
||
for k in scores.extra_keys
|
||
)
|
||
|
||
|
||
def test_skip_i2c_gpio_cc_regn():
|
||
assert skip_si_net("I2C_SDA")
|
||
assert skip_si_net("GPIO9")
|
||
assert skip_si_net("USB_CC1")
|
||
assert skip_si_net("/power/REGN")
|
||
assert skip_si_net("ESP32_EN")
|
||
assert not skip_si_net("USB_D+")
|
||
assert not skip_si_net("USB_DP")
|
||
assert bus_class("USB_D+") == "usb2"
|
||
assert bus_class("USB_SSTX_P") == "usb3"
|
||
assert bus_class("ETH_TX+") == "eth_mdi"
|
||
assert bus_class("RGMII_TXD0") == "rgmii"
|
||
assert bus_class("SGMII_TX_P") == "sgmii"
|
||
assert bus_class("DDR3_DQ0") == "ddr3_dq"
|
||
assert bus_class("DDR3_DQS0_P") == "ddr3_dqs"
|
||
assert bus_class("MIPI_D0_P") == "mipi"
|
||
assert bus_class("GTX_CLK") == "hf_clk"
|
||
assert bus_class("XTAL_IN") is None
|
||
assert bus_class("USB_CC1") is None
|
||
assert bus_class("I2C_SCL") is None
|
||
|
||
|
||
def _usb_graph() -> DesignGraph:
|
||
return DesignGraph(
|
||
components={
|
||
"U1": Component(
|
||
reference="U1", value="PHY", footprint="",
|
||
component_type=ComponentType.IC, mpn="PHY",
|
||
pins={"1": "USB_D+", "2": "USB_D-", "3": "USB_CC1", "4": "I2C_SDA"},
|
||
),
|
||
},
|
||
nets={
|
||
"USB_D+": Net(name="USB_D+", net_type=NetType.SIGNAL, pins=[
|
||
PinConnection(component_ref="U1", pin_number="1"),
|
||
]),
|
||
"USB_D-": Net(name="USB_D-", net_type=NetType.SIGNAL, pins=[
|
||
PinConnection(component_ref="U1", pin_number="2"),
|
||
]),
|
||
"USB_CC1": Net(name="USB_CC1", net_type=NetType.SIGNAL, pins=[]),
|
||
"I2C_SDA": Net(name="I2C_SDA", net_type=NetType.SIGNAL, pins=[]),
|
||
},
|
||
)
|
||
|
||
|
||
def _usb_layout() -> LayoutGraph:
|
||
return LayoutGraph(
|
||
segments=[
|
||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||
LayoutSegment(start=(0, 0.4), end=(12.5, 0.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
||
LayoutSegment(start=(0, 5), end=(8, 5), width=0.2, layer="F.Cu", net="USB_CC1"),
|
||
LayoutSegment(start=(0, 8), end=(20, 8), width=0.2, layer="F.Cu", net="I2C_SDA"),
|
||
],
|
||
vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)],
|
||
)
|
||
|
||
|
||
def _if_rows(**kwargs):
|
||
base = {
|
||
"net_name": "USB_D+",
|
||
"length_mm": 10.0,
|
||
"partner_net_name": "USB_D-",
|
||
"is_differential": True,
|
||
"z0_avg_ohms": 88.0,
|
||
"z0_min_ohms": 46.0,
|
||
"z0_max_ohms": 92.0,
|
||
"topologies": ["MICROSTRIP"],
|
||
"flags": (),
|
||
}
|
||
base.update(kwargs)
|
||
dm = {
|
||
"net_name": "USB_D-",
|
||
"length_mm": 12.5,
|
||
"partner_net_name": "USB_D+",
|
||
"is_differential": True,
|
||
"z0_avg_ohms": 88.0,
|
||
"z0_min_ohms": 46.0,
|
||
"z0_max_ohms": 92.0,
|
||
"topologies": ["MICROSTRIP"],
|
||
"flags": (),
|
||
}
|
||
return [base, dm]
|
||
|
||
|
||
def test_emmaforo_usb_avg_in_window_min_out_is_margin():
|
||
"""Zavg 88 Ω in 81–99, min 46 out → MARGIN; CC is not a 90 Ω pair."""
|
||
cons = ComponentConstraints(
|
||
mpn="PHY",
|
||
pintable=[Pin(number="1", name="D+")],
|
||
absolute_maximum_ratings=[],
|
||
rules=[],
|
||
layout_rules=[{
|
||
"kind": "impedance",
|
||
"net_class": "usb",
|
||
"zdiff_ohm": 90,
|
||
"tolerance_pct": 10,
|
||
"note": "USB DP/DM 90 Ω ±10%",
|
||
"source_page": 12,
|
||
}],
|
||
)
|
||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
|
||
zf = [f for f in findings if f.rule_id == "PE-SI-002"]
|
||
assert len(zf) == 1
|
||
assert zf[0].finding.startswith("MARGIN:")
|
||
complete_finding(zf[0])
|
||
assert zf[0].status == "WARNING"
|
||
assert zf[0].finding_class != "RULE"
|
||
assert not any("CC" in (f.net or "") for f in findings)
|
||
assert not any("I2C" in (f.net or "") for f in findings)
|
||
|
||
|
||
def test_usb_without_library_z_is_insufficient_not_90ohm_fail():
|
||
findings = check_si(_usb_graph(), {}, _usb_layout(), _if_rows())
|
||
ids = {f.rule_id for f in findings}
|
||
assert "PE-SI-010" in ids
|
||
assert "PE-SI-002" not in ids
|
||
f = next(x for x in findings if x.rule_id == "PE-SI-010")
|
||
assert f.evidence_status == "INSUFFICIENT"
|
||
assert f.status == "INFO"
|
||
assert "90" not in (f.requirement or "")
|
||
blob = f"{f.finding} {f.requirement} {f.inference}"
|
||
assert "FAIL" not in f.finding
|
||
assert "90 Ω" not in blob or "folklore" in (f.inference or "").lower()
|
||
assert "CC" not in (f.net or "")
|
||
|
||
|
||
def test_length_match_2_5mm_vs_1mm_is_fail():
|
||
cons = ComponentConstraints(
|
||
mpn="PHY",
|
||
pintable=[Pin(number="1", name="D+")],
|
||
absolute_maximum_ratings=[],
|
||
rules=[],
|
||
layout_rules=[{
|
||
"kind": "length_match",
|
||
"net_class": "usb",
|
||
"max_distance_mm": 1.0,
|
||
"note": "intra-pair < 1 mm",
|
||
"source_page": 12,
|
||
}],
|
||
)
|
||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
|
||
sk = [f for f in findings if f.rule_id == "PE-SI-001"]
|
||
assert sk and sk[0].finding.startswith("FAIL:")
|
||
complete_finding(sk[0])
|
||
assert sk[0].status == "ERROR"
|
||
|
||
|
||
def test_i2c_not_checked_as_50_ohm():
|
||
cons = ComponentConstraints(
|
||
mpn="PHY",
|
||
pintable=[Pin(number="1", name="D+")],
|
||
absolute_maximum_ratings=[],
|
||
rules=[],
|
||
layout_rules=[{
|
||
"kind": "impedance",
|
||
"z0_ohm": 50,
|
||
"tolerance_pct": 10,
|
||
"note": "should not hit I2C",
|
||
"source_page": 1,
|
||
}],
|
||
)
|
||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), [
|
||
*_if_rows(),
|
||
{"net_name": "I2C_SDA", "z0_avg_ohms": 120.0, "length_mm": 20.0},
|
||
])
|
||
assert all("I2C" not in (f.net or "") for f in findings)
|
||
assert all("SDA" not in f.finding for f in findings)
|
||
|
||
|
||
def test_en_rc_series_resistor_does_not_paint_usb():
|
||
"""PE-SI-009 must not use ESP32 EN RC 10 kΩ / 1 µF as a USB series R."""
|
||
cons = ComponentConstraints(
|
||
mpn="ESP32",
|
||
pintable=[Pin(number="1", name="D+"), Pin(number="3", name="EN")],
|
||
absolute_maximum_ratings=[],
|
||
rules=[],
|
||
layout_rules=[{
|
||
"kind": "series_resistor",
|
||
"pin": "EN",
|
||
"value_ohms": 10000,
|
||
"note": "EN RC 10 kΩ / 1 µF",
|
||
"source_page": 28,
|
||
}],
|
||
)
|
||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
|
||
si009 = [f for f in findings if f.rule_id == "PE-SI-009"]
|
||
assert si009 == []
|
||
zfail = [f for f in findings if f.rule_id == "PE-SI-002"]
|
||
assert zfail == []
|
||
si010 = [f for f in findings if f.rule_id == "PE-SI-010"]
|
||
assert si010
|
||
assert si010[0].evidence_status == "INSUFFICIENT"
|
||
assert "88" in (si010[0].finding or "")
|
||
assert "90" not in (si010[0].requirement or "")
|
||
|
||
|
||
def _phy_graph() -> DesignGraph:
|
||
pins = {
|
||
"1": "ETH_TX+", "2": "ETH_TX-",
|
||
"3": "RGMII_TXD0", "4": "RGMII_TXD1",
|
||
"5": "USB_SSTX_P", "6": "USB_SSTX_N",
|
||
"7": "DDR3_DQ0", "8": "DDR3_DQ1",
|
||
"9": "DDR3_DQS0_P", "10": "DDR3_DQS0_N",
|
||
"11": "USB_D+", "12": "USB_D-",
|
||
}
|
||
nets = {
|
||
n: Net(name=n, net_type=NetType.SIGNAL, pins=[
|
||
PinConnection(component_ref="U1", pin_number=p),
|
||
])
|
||
for p, n in pins.items()
|
||
}
|
||
return DesignGraph(
|
||
components={
|
||
"U1": Component(
|
||
reference="U1", value="PHY", footprint="",
|
||
component_type=ComponentType.IC, mpn="PHY",
|
||
pins=pins,
|
||
),
|
||
},
|
||
nets=nets,
|
||
)
|
||
|
||
|
||
def _phy_layout() -> LayoutGraph:
|
||
segs = [
|
||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="ETH_TX+"),
|
||
LayoutSegment(start=(0, 0.4), end=(10.2, 0.4), width=0.2, layer="F.Cu", net="ETH_TX-"),
|
||
LayoutSegment(start=(0, 2), end=(8, 2), width=0.15, layer="F.Cu", net="RGMII_TXD0"),
|
||
LayoutSegment(start=(0, 2.4), end=(8.4, 2.4), width=0.15, layer="F.Cu", net="RGMII_TXD1"),
|
||
LayoutSegment(start=(0, 4), end=(20, 4), width=0.12, layer="F.Cu", net="USB_SSTX_P"),
|
||
LayoutSegment(start=(0, 4.3), end=(20.5, 4.3), width=0.12, layer="F.Cu", net="USB_SSTX_N"),
|
||
LayoutSegment(start=(0, 6), end=(15, 6), width=0.1, layer="F.Cu", net="DDR3_DQ0"),
|
||
LayoutSegment(start=(0, 6.2), end=(15.1, 6.2), width=0.1, layer="F.Cu", net="DDR3_DQ1"),
|
||
LayoutSegment(start=(0, 7), end=(12, 7), width=0.1, layer="F.Cu", net="DDR3_DQS0_P"),
|
||
LayoutSegment(start=(0, 7.2), end=(12.3, 7.2), width=0.1, layer="F.Cu", net="DDR3_DQS0_N"),
|
||
LayoutSegment(start=(0, 9), end=(10, 9), width=0.2, layer="F.Cu", net="USB_D+"),
|
||
LayoutSegment(start=(0, 9.4), end=(12.5, 9.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
||
]
|
||
return LayoutGraph(segments=segs, vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)])
|
||
|
||
|
||
def _z(name, partner, avg, length, **kw):
|
||
row = {
|
||
"net_name": name,
|
||
"partner_net_name": partner,
|
||
"is_differential": True,
|
||
"z0_avg_ohms": avg,
|
||
"z0_min_ohms": avg - 2,
|
||
"z0_max_ohms": avg + 2,
|
||
"length_mm": length,
|
||
"topologies": ["MICROSTRIP"],
|
||
"flags": (),
|
||
}
|
||
row.update(kw)
|
||
return row
|
||
|
||
|
||
def test_coverage_mdi_rgmii_ddr3_usb3_gated_not_cross_mapped():
|
||
cons = ComponentConstraints(
|
||
mpn="PHY",
|
||
pintable=[Pin(number="1", name="TX+")],
|
||
absolute_maximum_ratings=[],
|
||
rules=[],
|
||
layout_rules=[
|
||
{
|
||
"kind": "impedance",
|
||
"net_class": "eth_mdi",
|
||
"zdiff_ohm": 100,
|
||
"tolerance_pct": 10,
|
||
"note": "MDI 100 Ω to RJ45",
|
||
"source_page": 40,
|
||
},
|
||
{
|
||
"kind": "max_length",
|
||
"net_class": "rgmii",
|
||
"max_distance_mm": 20,
|
||
"note": "RGMII MAC–PHY trace < 20 mm",
|
||
"source_page": 41,
|
||
},
|
||
{
|
||
"kind": "length_match",
|
||
"net_class": "ddr3",
|
||
"max_distance_mm": 1.0,
|
||
"note": "DDR3 DQS intra-pair",
|
||
"source_page": 42,
|
||
},
|
||
{
|
||
"kind": "impedance",
|
||
"net_class": "usb3",
|
||
"zdiff_ohm": 90,
|
||
"tolerance_pct": 10,
|
||
"note": "USB-C SuperSpeed 90 Ω",
|
||
"source_page": 43,
|
||
},
|
||
{
|
||
"kind": "series_resistor",
|
||
"net_class": "rgmii",
|
||
"value_ohms": 22,
|
||
"note": "RGMII series 22 Ω",
|
||
"source_page": 41,
|
||
},
|
||
],
|
||
)
|
||
rows = [
|
||
_z("ETH_TX+", "ETH_TX-", 95.0, 10.0),
|
||
_z("ETH_TX-", "ETH_TX+", 95.0, 10.2),
|
||
_z("USB_SSTX_P", "USB_SSTX_N", 88.0, 20.0),
|
||
_z("USB_SSTX_N", "USB_SSTX_P", 88.0, 20.5),
|
||
_z("DDR3_DQS0_P", "DDR3_DQS0_N", 50.0, 12.0),
|
||
_z("DDR3_DQS0_N", "DDR3_DQS0_P", 50.0, 12.3),
|
||
_z("USB_D+", "USB_D-", 88.0, 10.0),
|
||
_z("USB_D-", "USB_D+", 88.0, 12.5),
|
||
{"net_name": "RGMII_TXD0", "z0_avg_ohms": 48.0, "length_mm": 8.0, "topologies": ["MICROSTRIP"]},
|
||
{"net_name": "RGMII_TXD1", "z0_avg_ohms": 48.0, "length_mm": 8.4, "topologies": ["MICROSTRIP"]},
|
||
{"net_name": "DDR3_DQ0", "z0_avg_ohms": 50.0, "length_mm": 15.0, "topologies": ["MICROSTRIP"]},
|
||
]
|
||
findings = check_si(_phy_graph(), {"PHY": cons}, _phy_layout(), rows)
|
||
by = {}
|
||
for f in findings:
|
||
by.setdefault(f.rule_id, []).append(f)
|
||
|
||
mdi_z = [f for f in by.get("PE-SI-002", []) if f.net and "ETH" in f.net]
|
||
assert mdi_z and mdi_z[0].finding.startswith("PASS:")
|
||
usb3_z = [f for f in by.get("PE-SI-002", []) if f.net and "SSTX" in f.net]
|
||
assert usb3_z and usb3_z[0].finding.startswith("PASS:")
|
||
usb2_z = [f for f in by.get("PE-SI-002", []) if f.net and "USB_D" in (f.net or "")]
|
||
assert usb2_z == []
|
||
|
||
rgmii_len = [f for f in by.get("PE-SI-003", []) if f.net and "RGMII" in f.net]
|
||
assert rgmii_len and rgmii_len[0].finding.startswith("PASS:")
|
||
assert not any(f.net and "ETH_TX" in f.net for f in by.get("PE-SI-003", []))
|
||
|
||
dqs = [f for f in by.get("PE-SI-001", []) if f.net and "DQS" in f.net]
|
||
assert dqs and dqs[0].finding.startswith("PASS:")
|
||
|
||
si009 = by.get("PE-SI-009", [])
|
||
assert si009
|
||
assert all(f.net and "RGMII" in f.net for f in si009)
|
||
assert not any(f.net and "USB" in f.net for f in si009)
|
||
|
||
si010 = by.get("PE-SI-010", [])
|
||
nets010 = {f.net for f in si010}
|
||
assert any(n and "USB_D" in n for n in nets010)
|
||
assert not any(n and "ETH_TX" in n for n in nets010)
|
||
assert not any(n and "SSTX" in n for n in nets010)
|
||
|
||
|
||
def test_empty_net_class_impedance_does_not_paint_usb():
|
||
cons = ComponentConstraints(
|
||
mpn="PHY",
|
||
pintable=[Pin(number="1", name="D+")],
|
||
absolute_maximum_ratings=[],
|
||
rules=[],
|
||
layout_rules=[{
|
||
"kind": "impedance",
|
||
"z0_ohm": 50,
|
||
"tolerance_pct": 10,
|
||
"note": "generic 50 Ω with no bus",
|
||
"source_page": 1,
|
||
}],
|
||
)
|
||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
|
||
assert all(f.rule_id != "PE-SI-002" for f in findings)
|
||
assert any(f.rule_id == "PE-SI-010" for f in findings)
|