"""Tests for the SPICE netlist parser.""" from pathlib import Path import pytest from spice2wireviz.parser.netlist import parse_netlist FIXTURES = Path(__file__).parent / "fixtures" class TestBasicParsing: def test_parse_simple_board(self): netlist = parse_netlist(FIXTURES / "simple_board.net") assert "amplifier_board" in netlist.subcircuit_defs subckt = netlist.subcircuit_defs["amplifier_board"] assert subckt.port_names == ["VIN", "GND", "VOUT", "SIGNAL_IN"] def test_parse_boundary_components(self): netlist = parse_netlist(FIXTURES / "simple_board.net") subckt = netlist.subcircuit_defs["amplifier_board"] refs = [c.reference for c in subckt.boundary_components] assert "J1" in refs assert "J2" in refs assert "TP1" in refs assert len(subckt.boundary_components) == 3 def test_boundary_component_nodes(self): netlist = parse_netlist(FIXTURES / "simple_board.net") subckt = netlist.subcircuit_defs["amplifier_board"] j1 = next(c for c in subckt.boundary_components if c.reference == "J1") assert j1.nodes == ["VIN", "GND"] assert j1.value == "PWR_CONN" def test_test_point_single_node(self): netlist = parse_netlist(FIXTURES / "simple_board.net") subckt = netlist.subcircuit_defs["amplifier_board"] tp1 = next(c for c in subckt.boundary_components if c.reference == "TP1") assert tp1.nodes == ["N001"] assert tp1.prefix == "TP" class TestMultiBoardParsing: def test_parse_multi_board(self): netlist = parse_netlist(FIXTURES / "multi_board.net") assert len(netlist.subcircuit_defs) == 3 assert "power_supply" in netlist.subcircuit_defs assert "amplifier" in netlist.subcircuit_defs assert "io_board" in netlist.subcircuit_defs def test_instances(self): netlist = parse_netlist(FIXTURES / "multi_board.net") refs = [inst.reference for inst in netlist.instances] assert "X1" in refs assert "X2" in refs assert "X3" in refs def test_instance_port_mapping(self): netlist = parse_netlist(FIXTURES / "multi_board.net") x2 = next(i for i in netlist.instances if i.reference == "X2") assert x2.subcircuit_name == "amplifier" assert x2.port_to_net == {"VIN": "VCC", "GND": "GND", "VOUT": "AUDIO_OUT"} def test_top_level_components(self): netlist = parse_netlist(FIXTURES / "multi_board.net") refs = [c.reference for c in netlist.top_level_components] assert "J_CHASSIS" in refs assert "TP_VCC" in refs def test_top_level_connector_nodes(self): netlist = parse_netlist(FIXTURES / "multi_board.net") j_chassis = next(c for c in netlist.top_level_components if c.reference == "J_CHASSIS") assert "GND" in j_chassis.nodes assert "EARTH" in j_chassis.nodes class TestHierarchicalParsing: def test_global_nets(self): netlist = parse_netlist(FIXTURES / "hierarchical.net") assert "VCC" in netlist.global_nets assert "GND" in netlist.global_nets def test_continuation_lines(self): netlist = parse_netlist(FIXTURES / "hierarchical.net") reg = netlist.subcircuit_defs["regulator"] # The continuation line should have been joined assert "ENABLE" in reg.parameters or len(reg.port_names) >= 3 def test_inline_comments(self): """Inline ; comments should be stripped.""" netlist = parse_netlist(FIXTURES / "hierarchical.net") assert "main_board" in netlist.subcircuit_defs def test_all_subcircuits_found(self): netlist = parse_netlist(FIXTURES / "hierarchical.net") assert len(netlist.subcircuit_defs) == 3 assert "regulator" in netlist.subcircuit_defs assert "sensor_module" in netlist.subcircuit_defs assert "main_board" in netlist.subcircuit_defs class TestStringParsing: def test_parse_from_string(self): text = """\ .subckt simple A B J1 A B CONN .ends simple """ netlist = parse_netlist(text) assert "simple" in netlist.subcircuit_defs subckt = netlist.subcircuit_defs["simple"] assert subckt.port_names == ["A", "B"] assert len(subckt.boundary_components) == 1 def test_empty_netlist(self): netlist = parse_netlist("* Just a comment\n") assert len(netlist.subcircuit_defs) == 0 assert len(netlist.instances) == 0 def test_continuation_line_joining(self): text = """\ .subckt wide A B C + D E F J1 A B CONN .ends wide """ netlist = parse_netlist(text) subckt = netlist.subcircuit_defs["wide"] assert subckt.port_names == ["A", "B", "C", "D", "E", "F"] class TestNetClassification: def test_ground_nets(self): text = ".subckt test GND AGND DGND VCC\n.ends test\n" netlist = parse_netlist(text) assert netlist.is_ground_net("GND") assert netlist.is_ground_net("AGND") assert netlist.is_ground_net("0") assert not netlist.is_ground_net("VCC") def test_power_nets(self): text = ".subckt test VCC VDD GND\n.ends test\n" netlist = parse_netlist(text) assert netlist.is_power_net("VCC") assert netlist.is_power_net("VDD") assert not netlist.is_power_net("GND") assert not netlist.is_power_net("SIGNAL") def test_vss_is_ground_not_power(self): """VSS should be classified as ground (CMOS convention), not power.""" text = ".subckt test VSS VDD\n.ends test\n" netlist = parse_netlist(text) assert netlist.is_ground_net("VSS") assert not netlist.is_power_net("VSS") class TestEdgeCases: def test_nonexistent_file(self): with pytest.raises(FileNotFoundError): parse_netlist(Path("/nonexistent/file.net")) def test_missing_subcircuit_warning(self, capsys): text = """\ X1 A B C undefined_subckt """ netlist = parse_netlist(text) assert len(netlist.instances) == 1 captured = capsys.readouterr() assert "undefined_subckt" in captured.err def test_x_instance_without_subckt_def(self): text = "X1 NET1 NET2 NET3 mystery_chip\n" netlist = parse_netlist(text) inst = netlist.instances[0] assert inst.subcircuit_name == "mystery_chip" # Without definition, uses positional port names assert "port1" in inst.port_to_net assert inst.port_to_net["port1"] == "NET1" def test_port_count_mismatch_fewer_nodes(self, capsys): """C1: When instance has fewer nodes than subcircuit ports, warn and mark unconnected.""" text = """\ .subckt amp VIN GND VOUT ENABLE .ends amp X1 NET1 NET2 amp """ netlist = parse_netlist(text) inst = netlist.instances[0] captured = capsys.readouterr() assert "ERROR: port count mismatch" in captured.err assert "4 ports" in captured.err assert "2 nodes" in captured.err # Unconnected ports should be marked assert inst.port_to_net["VIN"] == "NET1" assert inst.port_to_net["GND"] == "NET2" assert "__UNCONNECTED_VOUT__" in inst.port_to_net["VOUT"] assert "__UNCONNECTED_ENABLE__" in inst.port_to_net["ENABLE"] def test_port_count_mismatch_more_nodes(self, capsys): """C1: When instance has more nodes than subcircuit ports, warn about extras.""" text = """\ .subckt small A B .ends small X1 NET1 NET2 NET3 NET4 small """ parse_netlist(text) captured = capsys.readouterr() assert "ERROR: port count mismatch" in captured.err assert "extra nodes" in captured.err def test_model_value_heuristic_warning(self, capsys): """I6: The model/value heuristic should warn when verbose is on.""" from spice2wireviz.parser import netlist as netlist_module original = netlist_module.verbose try: netlist_module.verbose = True text = """\ .subckt test A B J1 A B SOME_MODEL .ends test """ parse_netlist(text) captured = capsys.readouterr() assert "SOME_MODEL" in captured.err assert "model/value" in captured.err finally: netlist_module.verbose = original def test_model_value_heuristic_quiet_by_default(self, capsys): """The model/value heuristic should be silent when verbose is off.""" from spice2wireviz.parser import netlist as netlist_module original = netlist_module.verbose try: netlist_module.verbose = False text = """\ .subckt test A B J1 A B SOME_MODEL .ends test """ parse_netlist(text) captured = capsys.readouterr() assert "model/value" not in captured.err finally: netlist_module.verbose = original def test_duplicate_refs_both_parsed(self): """S3: Duplicate reference designators should both be parsed.""" text = """\ J1 NET_A NET_B CONN_TYPE J1 NET_C NET_D CONN_TYPE """ netlist = parse_netlist(text) # Both should appear (parser doesn't deduplicate) j1_refs = [c for c in netlist.top_level_components if c.reference == "J1"] assert len(j1_refs) == 2