Add BOM output, fix LTspice Tier 2 import, real .asc integration tests
- Fix _try_ltspice_generation() to use spicelib.simulators.ltspice_simulator.LTspice instead of the abstract Simulator base class (which always returned unavailable) - Use LTspice.create_netlist() instead of Simulator.run() for correct netlist generation - Add --ltspice-exe CLI option to specify LTspice binary path - Add --bom flag for component BOM CSV output (works on any parse completeness) - Add --bom-wiring flag for wiring BOM CSV from mapped output - Add real 1002A.asc demo circuit and pre-generated .net as test fixtures - Add @pytest.mark.ltspice marker for tests requiring LTspice binary - Bump version to 2026.2.14
This commit is contained in:
parent
08c92bfefb
commit
5a5337566c
11 changed files with 607 additions and 14 deletions
|
|
@ -206,8 +206,7 @@ class TestLTspiceGeneration:
|
|||
result = parse_asc(asc)
|
||||
assert result.completeness == DataCompleteness.METADATA_ONLY
|
||||
|
||||
@patch("spice2wireviz.parser.asc.Simulator", create=True)
|
||||
def test_ltspice_generation_success(self, mock_sim_cls, tmp_path):
|
||||
def test_ltspice_generation_success(self, tmp_path):
|
||||
"""Mocked LTspice successfully generates a .net file."""
|
||||
asc = tmp_path / "test.asc"
|
||||
asc.write_text("Version 4\nSHEET 1 880 680\n")
|
||||
|
|
@ -220,7 +219,7 @@ class TestLTspiceGeneration:
|
|||
".ends gen_mod\n"
|
||||
)
|
||||
|
||||
# Mock the import path inside _try_ltspice_generation
|
||||
# Mock the whole Tier 2 function
|
||||
with patch(
|
||||
"spice2wireviz.parser.asc._try_ltspice_generation"
|
||||
) as mock_tier2:
|
||||
|
|
@ -279,3 +278,89 @@ class TestCompanionNetlistInternalHelper:
|
|||
assert result is not None
|
||||
assert result.completeness == DataCompleteness.FULL
|
||||
assert result.source_net == net
|
||||
|
||||
|
||||
class TestRealAscIntegration:
|
||||
"""Integration tests with a real LTspice-generated .asc / .net pair.
|
||||
|
||||
1002A.asc is a two op-amp instrumentation amplifier from the LTspice
|
||||
demo circuit archive. The companion 1002A.net was pre-generated by
|
||||
LTspice so these tests work in CI without LTspice installed.
|
||||
"""
|
||||
|
||||
def test_real_asc_companion_resolution(self):
|
||||
"""Tier 1: 1002A.asc resolves to companion 1002A.net."""
|
||||
result = parse_asc(FIXTURES / "1002A.asc")
|
||||
assert result.completeness == DataCompleteness.FULL
|
||||
assert result.source_net is not None
|
||||
assert result.source_net.name == "1002A.net"
|
||||
|
||||
def test_real_asc_has_components(self):
|
||||
"""The parsed netlist has X* instances (op-amps)."""
|
||||
result = parse_asc(FIXTURES / "1002A.asc")
|
||||
netlist = result.netlist
|
||||
# 1002A.net has X§U1 and X§U2 (LTspice op-amp instances)
|
||||
x_refs = [inst.reference for inst in netlist.instances]
|
||||
assert len(x_refs) >= 2
|
||||
# The § character is part of LTspice's hierarchical naming
|
||||
assert any("U1" in ref for ref in x_refs)
|
||||
assert any("U2" in ref for ref in x_refs)
|
||||
|
||||
def test_real_asc_has_nets(self):
|
||||
"""The parsed netlist has known nets from the circuit."""
|
||||
result = parse_asc(FIXTURES / "1002A.asc")
|
||||
netlist = result.netlist
|
||||
assert len(netlist.all_nets) > 0
|
||||
# The circuit uses +V, -V, OUT, IN+, IN- as net names
|
||||
net_names = {n.upper() for n in netlist.all_nets}
|
||||
assert "+V" in net_names or "V+" in net_names or any("V" in n for n in net_names)
|
||||
|
||||
def test_real_asc_no_warnings_on_companion(self):
|
||||
"""Companion resolution should produce no warnings."""
|
||||
result = parse_asc(FIXTURES / "1002A.asc")
|
||||
# Warnings are acceptable for enrichment, but not for core parsing
|
||||
# Only check that we didn't get "no companion" warnings
|
||||
for w in result.warnings:
|
||||
assert "no companion" not in w.lower()
|
||||
|
||||
@pytest.mark.ltspice
|
||||
def test_real_asc_ltspice_generation(self, tmp_path):
|
||||
"""Tier 2: generate .net from .asc using LTspice binary.
|
||||
|
||||
Requires LTspice installed. Skipped in CI.
|
||||
"""
|
||||
import shutil
|
||||
|
||||
ltspice_path = Path("/home/rpm/.local/bin/ltspice")
|
||||
if not ltspice_path.exists():
|
||||
pytest.skip("LTspice binary not found")
|
||||
|
||||
# Copy .asc to temp dir (avoid polluting fixtures)
|
||||
asc_copy = tmp_path / "1002A.asc"
|
||||
shutil.copy2(FIXTURES / "1002A.asc", asc_copy)
|
||||
|
||||
# Remove any existing .net so Tier 2 is forced
|
||||
net_copy = tmp_path / "1002A.net"
|
||||
if net_copy.exists():
|
||||
net_copy.unlink()
|
||||
|
||||
result = parse_asc(
|
||||
asc_copy,
|
||||
allow_ltspice_generation=True,
|
||||
ltspice_exe=str(ltspice_path),
|
||||
)
|
||||
assert result.completeness == DataCompleteness.FULL
|
||||
assert result.source_net is not None
|
||||
assert result.source_net.name == "1002A.net"
|
||||
|
||||
def test_ltspice_exe_nonexistent_path(self, tmp_path):
|
||||
"""--ltspice-exe with a bad path falls through to Tier 3."""
|
||||
asc = tmp_path / "test.asc"
|
||||
asc.write_text("Version 4\nSHEET 1 880 680\n")
|
||||
|
||||
result = parse_asc(
|
||||
asc,
|
||||
allow_ltspice_generation=True,
|
||||
ltspice_exe="/nonexistent/ltspice",
|
||||
)
|
||||
assert result.completeness == DataCompleteness.METADATA_ONLY
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue