spice2wireviz/tests/test_roundtrip.py
Ryan Malloy e20a956f51 Initial project structure for spice2wireviz
SPICE netlist to WireViz YAML converter with:
- Custom lightweight netlist parser (.net/.cir/.sp)
- Single-module mapper (subcircuit external interface)
- Inter-module mapper (multi-board wiring)
- Filter engine with glob patterns
- Click CLI with auto-detection, inspection commands
- Optional .asc parser via spicelib
- Comprehensive test suite with fixtures
2026-02-13 01:24:41 -07:00

146 lines
5.5 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"
@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