Address all critical and important findings from safety review
C1: Port count mismatch now emits ERROR to stderr and marks
unconnected ports as __UNCONNECTED_<name>__ (never silent)
C2: Single-module pin mapping built in lockstep — cable_wires
always matches header_pins length, warns on unmappable nets
C3: VSS removed from is_power_net (it's ground per CMOS convention),
dead _POWER_PATTERN regex replaced with _KNOWN_NET_PATTERN
C4: apply_filters dead computation removed, docstring clarifies
that net-level filtering is a mapper concern
I3: WireViz render catches (ValueError, TypeError, OSError) with
diagnostic context instead of bare Exception
I5: Invalid --format flags now warn and error instead of silently
falling through to defaults
I6: Model/value heuristic warns on stderr when it triggers, since
signal names like ALERT or DATA could be misidentified
New tests: VSS classification, port count mismatch (C1), model/value
heuristic warning (I6), duplicate refs (S3), empty subcircuit (S2),
full pipeline determinism across 3 runs (S1)
115 tests pass, ruff clean
This commit is contained in:
parent
eb3ad60bd1
commit
b9154e851b
7 changed files with 254 additions and 46 deletions
|
|
@ -146,6 +146,13 @@ class TestNetClassification:
|
|||
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):
|
||||
|
|
@ -169,3 +176,57 @@ X1 A B C undefined_subckt
|
|||
# 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 it triggers."""
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -97,6 +97,77 @@ class TestYamlValidity:
|
|||
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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue