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
This commit is contained in:
commit
e20a956f51
24 changed files with 2630 additions and 0 deletions
171
tests/test_netlist_parser.py
Normal file
171
tests/test_netlist_parser.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"""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")
|
||||
|
||||
|
||||
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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue