spice2wireviz/tests/test_roundtrip.py

217 lines
8 KiB
Python
Raw Permalink Normal View History

"""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