"""Integration tests for LtspiceEngine with RC circuits. Skipped automatically when mcltspice + Wine + LTspice aren't available (e.g., Docker production, CI). """ import tempfile from pathlib import Path import pytest # Determine availability at import time for skip markers _ltspice_available = False try: from mcltspice.config import validate_installation _ltspice_available, _ = validate_installation() except ImportError: pass pytestmark = pytest.mark.skipif( not _ltspice_available, reason="LTspice not available (mcltspice + Wine + LTspice.exe required)", ) from spicebook.engine.ltspice import LtspiceEngine # noqa: E402 RC_TRANSIENT_NETLIST = """\ RC Low-Pass Filter - Transient V1 in 0 PULSE(0 5 0 1n 1n 5m 10m) R1 in out 1k C1 out 0 1u .tran 100u 20m .end """ RC_AC_NETLIST = """\ RC Low-Pass Filter - AC V1 in 0 AC 1 R1 in out 1k C1 out 0 1u .ac dec 50 1 1Meg .end """ @pytest.fixture def engine(): return LtspiceEngine() @pytest.fixture def work_dir(): d = tempfile.mkdtemp(prefix="spicebook-ltspice-test-") yield Path(d) import shutil shutil.rmtree(d, ignore_errors=True) @pytest.mark.asyncio async def test_transient_simulation(engine, work_dir): """Run a transient analysis via LTspice and verify waveform output.""" result = await engine.run(RC_TRANSIENT_NETLIST, work_dir) assert result.success, f"Simulation failed: {result.error}\n{result.log}" assert result.waveform is not None assert result.waveform.x_type == "time" assert result.waveform.points > 0 assert len(result.waveform.x_data) == result.waveform.points assert result.elapsed_seconds > 0 # Should have at least one signal in y_data assert len(result.waveform.y_data) > 0 # Check that V(out) or similar exists signal_names = list(result.waveform.y_data.keys()) has_out = any("out" in name.lower() for name in signal_names) assert has_out, f"Expected 'out' signal in {signal_names}" # Verify data length matches for name, values in result.waveform.y_data.items(): assert len(values) == result.waveform.points, f"Length mismatch for {name}" @pytest.mark.asyncio async def test_ac_simulation(engine, work_dir): """Run an AC analysis via LTspice and verify complex waveform output.""" result = await engine.run(RC_AC_NETLIST, work_dir) assert result.success, f"Simulation failed: {result.error}\n{result.log}" assert result.waveform is not None assert result.waveform.x_type == "frequency" assert result.waveform.is_complex assert result.waveform.points > 0 # AC analysis should produce magnitude and phase data assert result.waveform.y_magnitude_db is not None assert result.waveform.y_phase_deg is not None assert len(result.waveform.y_magnitude_db) > 0 assert len(result.waveform.y_phase_deg) > 0 @pytest.mark.asyncio async def test_netlist_without_end_directive(engine, work_dir): """Engine should auto-append .end if missing.""" netlist_no_end = """\ Simple Resistor Divider V1 in 0 DC 10 R1 in out 1k R2 out 0 1k .tran 1u 1m """ result = await engine.run(netlist_no_end, work_dir) assert result.success, f"Simulation failed: {result.error}\n{result.log}" @pytest.mark.asyncio async def test_invalid_netlist(engine, work_dir): """Malformed netlist should return success=False with error info.""" bad_netlist = """\ This is not a valid SPICE netlist .tran 1m 10m .end """ result = await engine.run(bad_netlist, work_dir) assert isinstance(result.success, bool)