Files
periscope/tests/test_edif_parser.py
T
Siddharth Kothari 6672d2be57 Pinscope open-source core
Agentic schematic validation: datasheet extraction via Claude Console
Skills, netlist/BOM design graph, per-IC direct datasheet review with
page citations, capacitor derating, Next.js report UI.

Extracted from the Pinscope cloud codebase. Auth and billing live in the
private gateway repo behind stable seams (billing_hook.py, adapter files
listed in CLAUDE.md).
2026-07-16 21:29:45 -07:00

180 lines
6.4 KiB
Python

"""EDIF 2.0.0 netlist parser — Siemens xDX Designer flavor.
Verified against the client-supplied file ``edif-files/144040 (1).edn``
(128 KB, two sub-designs merged, 42 instances, 20 nets). The BOM at
``edif-files/BOM (1).xlsx`` covers 19 of those 42 designators; the rest are
orphan parts in the second sub-design and are expected to land in the graph
unchanged.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from backend.pinscopex.parsers import (
detect_netlist_format,
parse_netlist_any,
validate_netlist,
)
from backend.pinscopex.parsers_edif import (
list_edif_subdesigns,
parse_edif_netlist,
)
EDIF_FIXTURE = Path(__file__).resolve().parent.parent / "edif-files" / "144040 (1).edn"
# The client-supplied EDIF sample is confidential and never committed —
# these tests only run on machines that have it locally.
if not EDIF_FIXTURE.exists():
pytest.skip(
"edif-files/ sample netlist not present (local-only, untracked)",
allow_module_level=True,
)
# Designators the client's BOM lists. The parser MUST surface every one of
# these — anything else is a regression.
BOM_DESIGNATORS = frozenset({
"C1", "C2", "C3", "C4", "C5", "C6",
"C22", "C23", "C24", "C25",
"R1", "R6", "R7",
"L1", "L5",
"FB1", "FB2",
"U1", "U3",
})
@pytest.fixture(scope="module")
def parsed():
parts, nets = parse_edif_netlist(EDIF_FIXTURE)
return parts, nets
def test_format_detection():
assert detect_netlist_format(EDIF_FIXTURE.read_bytes()) == "edif"
def test_dispatcher_routes_to_edif():
parts, nets, fmt = parse_netlist_any(EDIF_FIXTURE)
assert fmt == "edif"
assert parts and nets
def test_all_bom_designators_present(parsed):
parts, _ = parsed
missing = BOM_DESIGNATORS - set(parts)
assert not missing, f"BOM designators missing from parser output: {sorted(missing)}"
def test_u3_pin_count_matches_ldo(parsed):
"""U3 is the MIC5305YMLTR LDO — datasheet has 7 pins (6 + thermal EPAD)."""
_, nets = parsed
u3_pins = {pin for conns in nets.values() for ref, pin in conns if ref == "U3"}
assert len(u3_pins) == 7, f"expected 7 distinct pins for U3, got {sorted(u3_pins)}"
def test_u1_pin_count_matches_rf_amp(parsed):
"""U1 is the CMD263P3 RF amp — datasheet has 17 pins (16 + thermal EPAD)."""
_, nets = parsed
u1_pins = {pin for conns in nets.values() for ref, pin in conns if ref == "U1"}
assert len(u1_pins) == 17, f"expected 17 distinct pins for U1, got {sorted(u1_pins)}"
def test_ground_net_renamed(parsed):
"""The parser must rename Pin_Type=GROUND nets to ``GND`` so the existing
validate_netlist() ground check passes."""
_, nets = parsed
assert "GND" in nets, f"expected a 'GND' net, found: {sorted(nets)}"
assert len(nets["GND"]) > 5, "GND should have many endpoints in a real design"
def test_validate_netlist_accepts_edif_output(parsed):
parts, nets = parsed
issues = validate_netlist(parts, nets)
assert issues == [], f"unexpected validation issues: {issues}"
def test_every_net_endpoint_resolves(parsed):
"""No dangling (ref, pin) tuples — every connection should reference a
designator that's also in the parts dict."""
parts, nets = parsed
refs = set(parts)
bad = [
(net, ref, pin)
for net, conns in nets.items()
for ref, pin in conns
if ref not in refs
]
assert not bad, f"net endpoints reference unknown designators: {bad[:5]}"
def test_template_designators_skipped(parsed):
"""Instances whose designator is still a template (``R?`` / ``C?`` / ``U?``)
should not leak into parts — they're unconfigured library symbols."""
parts, _ = parsed
leaked = [ref for ref in parts if ref.endswith("?")]
assert not leaked, f"template designators leaked: {leaked}"
# ---------------------------------------------------------------------------
# Sub-design listing + filtering
# ---------------------------------------------------------------------------
def test_list_subdesigns_finds_two():
"""The client's file has two sub-designs (&0441, &0442). The lister must
surface both with their instance counts and full designator lists."""
subs = list_edif_subdesigns(EDIF_FIXTURE)
ids = sorted(s["id"] for s in subs if s["id"])
assert ids == ["&0441", "&0442"], f"unexpected sub-design ids: {ids}"
# Each sub-design carries 21 resolved-designator instances.
for s in subs:
assert s["instance_count"] == len(s["designators"])
assert s["instance_count"] > 0
def test_subdesign_filter_keeps_only_selected():
"""Filtering to ``&0441`` must yield exactly that sub-design's parts."""
parts_a, nets_a = parse_edif_netlist(EDIF_FIXTURE, include_subdesigns={"&0441"})
parts_b, nets_b = parse_edif_netlist(EDIF_FIXTURE, include_subdesigns={"&0442"})
bom = BOM_DESIGNATORS
in_a = set(parts_a) & bom
in_b = set(parts_b) & bom
assert in_a == bom, f"sub-design A should hold every BOM ref; missing: {bom - in_a}"
assert in_b == set(), f"sub-design B has 0 BOM refs in the fixture; got: {in_b}"
# Designators must not overlap across sub-designs (xDX gives each its own
# ref namespace per board).
assert set(parts_a).isdisjoint(parts_b)
def test_subdesign_filter_preserves_shared_ground():
"""``GND`` (renamed via Pin_Type detection) spans both sub-designs in the
raw EDIF. After filtering to one sub-design the net survives, but only
with endpoints from instances that survived."""
_, nets_a = parse_edif_netlist(EDIF_FIXTURE, include_subdesigns={"&0441"})
_, nets_all = parse_edif_netlist(EDIF_FIXTURE)
assert "GND" in nets_a
refs_a = {ref for ref, _ in nets_a["GND"]}
refs_all = {ref for ref, _ in nets_all["GND"]}
# Filtered GND is a strict subset of unfiltered GND.
assert refs_a < refs_all, "filtered GND should drop the excluded sub-design's endpoints"
def test_subdesign_filter_empty_set_returns_empty():
"""Empty selection yields no parts (and no nets that referenced them)."""
parts, nets = parse_edif_netlist(EDIF_FIXTURE, include_subdesigns=set())
assert parts == {}
assert nets == {}
def test_subdesign_filter_none_matches_unfiltered():
"""``include_subdesigns=None`` (the default) is the pre-flag behavior."""
parts1, nets1 = parse_edif_netlist(EDIF_FIXTURE)
parts2, nets2 = parse_edif_netlist(EDIF_FIXTURE, include_subdesigns=None)
assert parts1 == parts2
assert nets1 == nets2