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