C1: Port count mismatch now emits ERROR to stderr and marks
unconnected ports as __UNCONNECTED_<name>__ (never silent)
C2: Single-module pin mapping built in lockstep — cable_wires
always matches header_pins length, warns on unmappable nets
C3: VSS removed from is_power_net (it's ground per CMOS convention),
dead _POWER_PATTERN regex replaced with _KNOWN_NET_PATTERN
C4: apply_filters dead computation removed, docstring clarifies
that net-level filtering is a mapper concern
I3: WireViz render catches (ValueError, TypeError, OSError) with
diagnostic context instead of bare Exception
I5: Invalid --format flags now warn and error instead of silently
falling through to defaults
I6: Model/value heuristic warns on stderr when it triggers, since
signal names like ALERT or DATA could be misidentified
New tests: VSS classification, port count mismatch (C1), model/value
heuristic warning (I6), duplicate refs (S3), empty subcircuit (S2),
full pipeline determinism across 3 runs (S1)
115 tests pass, ruff clean
217 lines
8.0 KiB
Python
217 lines
8.0 KiB
Python
"""Roundtrip tests: parse SPICE -> emit YAML -> validate with WireViz.
|
|
|
|
These tests verify the generated YAML is structurally valid by feeding
|
|
it to wireviz.wireviz.parse() and checking it doesn't raise exceptions.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from spice2wireviz.emitter.yaml_emitter import emit_yaml
|
|
from spice2wireviz.filter import FilterConfig
|
|
from spice2wireviz.mapper.inter_module import map_inter_module
|
|
from spice2wireviz.mapper.single_module import map_single_module
|
|
from spice2wireviz.parser.netlist import parse_netlist
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
|
|
|
|
def _has_wireviz() -> bool:
|
|
try:
|
|
from wireviz.wireviz import parse # noqa: F401
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
|
|
class TestYamlValidity:
|
|
"""Verify generated YAML is valid YAML and has required WireViz keys."""
|
|
|
|
def test_single_module_yaml_structure(self):
|
|
netlist = parse_netlist(FIXTURES / "simple_board.net")
|
|
result = map_single_module(netlist, "amplifier_board")
|
|
yaml_str = emit_yaml(result)
|
|
parsed = yaml.safe_load(yaml_str)
|
|
|
|
assert "connectors" in parsed
|
|
assert "cables" in parsed
|
|
assert "connections" in parsed
|
|
assert isinstance(parsed["connectors"], dict)
|
|
assert isinstance(parsed["cables"], dict)
|
|
assert isinstance(parsed["connections"], list)
|
|
|
|
def test_inter_module_yaml_structure(self):
|
|
netlist = parse_netlist(FIXTURES / "multi_board.net")
|
|
result = map_inter_module(netlist)
|
|
yaml_str = emit_yaml(result)
|
|
parsed = yaml.safe_load(yaml_str)
|
|
|
|
assert "connectors" in parsed
|
|
assert "cables" in parsed
|
|
assert "connections" in parsed
|
|
|
|
def test_hierarchical_yaml_structure(self):
|
|
netlist = parse_netlist(FIXTURES / "hierarchical.net")
|
|
result = map_inter_module(netlist)
|
|
yaml_str = emit_yaml(result)
|
|
parsed = yaml.safe_load(yaml_str)
|
|
|
|
assert "connectors" in parsed
|
|
assert len(parsed["connectors"]) >= 6 # 3 instances + 3 top-level
|
|
|
|
def test_connector_has_pin_spec(self):
|
|
"""Every connector must have at least one of pincount/pins/pinlabels."""
|
|
netlist = parse_netlist(FIXTURES / "multi_board.net")
|
|
result = map_inter_module(netlist)
|
|
yaml_str = emit_yaml(result)
|
|
parsed = yaml.safe_load(yaml_str)
|
|
|
|
for name, conn in parsed["connectors"].items():
|
|
has_pins = any(k in conn for k in ("pincount", "pins", "pinlabels"))
|
|
assert has_pins, f"Connector '{name}' missing pin specification"
|
|
|
|
def test_cable_has_wire_spec(self):
|
|
"""Every cable must have wirecount or colors."""
|
|
netlist = parse_netlist(FIXTURES / "multi_board.net")
|
|
result = map_inter_module(netlist)
|
|
yaml_str = emit_yaml(result)
|
|
parsed = yaml.safe_load(yaml_str)
|
|
|
|
for name, cable in parsed["cables"].items():
|
|
has_wires = "wirecount" in cable or "colors" in cable
|
|
assert has_wires, f"Cable '{name}' missing wire specification"
|
|
|
|
def test_connection_set_structure(self):
|
|
"""Each connection set should be a list of dicts."""
|
|
netlist = parse_netlist(FIXTURES / "multi_board.net")
|
|
result = map_inter_module(netlist)
|
|
yaml_str = emit_yaml(result)
|
|
parsed = yaml.safe_load(yaml_str)
|
|
|
|
for i, conn_set in enumerate(parsed["connections"]):
|
|
assert isinstance(conn_set, list), f"Connection set {i} is not a list"
|
|
assert len(conn_set) == 3, f"Connection set {i} doesn't have 3 elements"
|
|
for entry in conn_set:
|
|
assert isinstance(entry, dict), f"Connection set {i} entry is not a dict"
|
|
|
|
|
|
class TestPipelineDeterminism:
|
|
"""S1: Verify the full pipeline produces byte-identical output on repeated runs."""
|
|
|
|
def test_inter_module_deterministic(self):
|
|
"""Parse the same file multiple times; output must be identical."""
|
|
outputs = []
|
|
for _ in range(3):
|
|
netlist = parse_netlist(FIXTURES / "hierarchical.net")
|
|
result = map_inter_module(netlist)
|
|
yaml_str = emit_yaml(result)
|
|
outputs.append(yaml_str)
|
|
assert outputs[0] == outputs[1] == outputs[2]
|
|
|
|
def test_single_module_deterministic(self):
|
|
outputs = []
|
|
for _ in range(3):
|
|
netlist = parse_netlist(FIXTURES / "simple_board.net")
|
|
result = map_single_module(netlist, "amplifier_board")
|
|
yaml_str = emit_yaml(result)
|
|
outputs.append(yaml_str)
|
|
assert outputs[0] == outputs[1] == outputs[2]
|
|
|
|
def test_filtered_deterministic(self):
|
|
config = FilterConfig(show_ground=False, show_power=False)
|
|
outputs = []
|
|
for _ in range(3):
|
|
netlist = parse_netlist(FIXTURES / "multi_board.net")
|
|
result = map_inter_module(netlist, config)
|
|
yaml_str = emit_yaml(result)
|
|
outputs.append(yaml_str)
|
|
assert outputs[0] == outputs[1] == outputs[2]
|
|
|
|
|
|
class TestEmptySubcircuit:
|
|
"""S2: Subcircuit with ports but no boundary components."""
|
|
|
|
def test_empty_subcircuit_produces_header_only(self):
|
|
text = """\
|
|
.subckt bare_module A B C
|
|
* No J*/TP*/P* inside — just passives
|
|
R1 A B 10k
|
|
.ends bare_module
|
|
"""
|
|
netlist = parse_netlist(text)
|
|
result = map_single_module(netlist, "bare_module")
|
|
# Should produce a header connector but no cables or connections
|
|
assert "bare_module" in result["connectors"]
|
|
assert len(result["cables"]) == 0
|
|
assert len(result["connections"]) == 0
|
|
|
|
|
|
class TestDuplicateRefInMapper:
|
|
"""S3: Duplicate reference designators in the mapper."""
|
|
|
|
def test_duplicate_ref_last_wins_in_inter_module(self):
|
|
"""When two top-level components share a reference, the mapper
|
|
builds connectors from both but dict key collision means last wins.
|
|
This is a known limitation that should at least not crash."""
|
|
text = """\
|
|
.subckt mod A B
|
|
.ends mod
|
|
X1 NET1 NET2 mod
|
|
J1 NET1 NET2 CONN_A
|
|
J1 NET3 NET4 CONN_B
|
|
"""
|
|
netlist = parse_netlist(text)
|
|
result = map_inter_module(netlist)
|
|
# Should not crash; J1 connector exists (last definition wins)
|
|
assert "J1" in result["connectors"]
|
|
|
|
|
|
@pytest.mark.skipif(not _has_wireviz(), reason="WireViz not installed")
|
|
class TestWireVizRoundtrip:
|
|
"""Feed generated YAML to WireViz's parse() to verify full compatibility."""
|
|
|
|
def test_single_module_roundtrip(self):
|
|
from wireviz.wireviz import parse as wv_parse
|
|
|
|
netlist = parse_netlist(FIXTURES / "simple_board.net")
|
|
result = map_single_module(netlist, "amplifier_board")
|
|
# WireViz parse() accepts dicts directly
|
|
harness = wv_parse(result, return_types="harness")
|
|
assert harness is not None
|
|
|
|
def test_inter_module_roundtrip(self):
|
|
from wireviz.wireviz import parse as wv_parse
|
|
|
|
netlist = parse_netlist(FIXTURES / "multi_board.net")
|
|
result = map_inter_module(netlist)
|
|
harness = wv_parse(result, return_types="harness")
|
|
assert harness is not None
|
|
|
|
def test_hierarchical_roundtrip(self):
|
|
from wireviz.wireviz import parse as wv_parse
|
|
|
|
netlist = parse_netlist(FIXTURES / "hierarchical.net")
|
|
result = map_inter_module(netlist)
|
|
harness = wv_parse(result, return_types="harness")
|
|
assert harness is not None
|
|
|
|
def test_filtered_roundtrip(self):
|
|
from wireviz.wireviz import parse as wv_parse
|
|
|
|
netlist = parse_netlist(FIXTURES / "hierarchical.net")
|
|
config = FilterConfig(show_ground=False, show_power=False)
|
|
result = map_inter_module(netlist, config)
|
|
harness = wv_parse(result, return_types="harness")
|
|
assert harness is not None
|
|
|
|
def test_render_svg(self, tmp_path):
|
|
from wireviz.wireviz import parse as wv_parse
|
|
|
|
netlist = parse_netlist(FIXTURES / "multi_board.net")
|
|
result = map_inter_module(netlist)
|
|
svg = wv_parse(result, return_types="svg")
|
|
assert svg is not None
|
|
assert len(svg) > 100 # Should be a real SVG
|