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:
Ryan Malloy 2026-02-13 01:34:30 -07:00
parent eb3ad60bd1
commit b9154e851b
7 changed files with 254 additions and 46 deletions

View file

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