"""Tests for the SpiceRunner interface and NgspiceRunner.""" import os import pytest from pyngspice.runner import NgspiceRunner, SubprocessRunner, SimulationError, get_runner class TestNgspiceRunnerDetect: """Test NgspiceRunner.detect() and get_executable_path().""" def test_detect(self): """NgspiceRunner should detect the embedded C++ extension.""" from pyngspice import _cpp_available assert NgspiceRunner.detect() == _cpp_available def test_executable_path_is_none(self): """Embedded runner has no separate executable.""" assert NgspiceRunner.get_executable_path() is None @pytest.mark.skipif( not NgspiceRunner.detect(), reason="C++ extension not built" ) class TestNgspiceRunnerSimulation: """Integration tests for NgspiceRunner (require built C++ extension).""" def test_run_simple_rc(self, rc_netlist, tmp_workdir): """Run a simple RC circuit and verify output files exist.""" runner = NgspiceRunner(working_directory=tmp_workdir) raw_file, log_file = runner.run(rc_netlist) assert os.path.isfile(raw_file) assert raw_file.endswith(".raw") def test_run_with_rser_preprocessing(self, inductor_rser_netlist, tmp_workdir): """Run a netlist with Rser= inductors (tests pre-processing).""" runner = NgspiceRunner(working_directory=tmp_workdir) raw_file, log_file = runner.run(inductor_rser_netlist) assert os.path.isfile(raw_file) def test_run_returns_absolute_paths(self, rc_netlist, tmp_workdir): """Output paths should be absolute.""" runner = NgspiceRunner(working_directory=tmp_workdir) raw_file, log_file = runner.run(rc_netlist) assert os.path.isabs(raw_file) assert os.path.isabs(log_file) def test_run_with_savecurrents(self, tesla_coil_netlist, tmp_workdir): """Run Tesla coil netlist with .options savecurrents — verify cap currents.""" runner = NgspiceRunner(working_directory=tmp_workdir) raw_file, log_file = runner.run(tesla_coil_netlist) assert os.path.isfile(raw_file) assert raw_file.endswith(".raw") # Verify raw file contains renamed capacitor current traces from pyngspice import RawRead raw = RawRead(raw_file) trace_names = [n.lower() for n in raw.get_trace_names()] # Capacitor currents should appear with original names (post-processed) assert 'i(c_mmc)' in trace_names, f"Missing i(c_mmc) in {trace_names}" assert 'i(c_topload)' in trace_names, f"Missing i(c_topload) in {trace_names}" # Probe names should NOT appear (renamed away) assert 'i(v_probe_c_mmc)' not in trace_names assert 'i(v_probe_c_topload)' not in trace_names def test_run_nonexistent_netlist(self, tmp_workdir): """Should raise FileNotFoundError for missing netlist.""" runner = NgspiceRunner(working_directory=tmp_workdir) with pytest.raises(FileNotFoundError): runner.run("/nonexistent/file.net") def test_working_directory_created(self, rc_netlist): """Working directory should be created if it doesn't exist.""" import tempfile workdir = os.path.join(tempfile.gettempdir(), "pyngspice_test_auto_create") try: runner = NgspiceRunner(working_directory=workdir) assert os.path.isdir(workdir) finally: if os.path.isdir(workdir): os.rmdir(workdir) class TestSubprocessRunner: """Tests for SubprocessRunner.""" def test_detect(self): """detect() should return bool without error.""" result = SubprocessRunner.detect() assert isinstance(result, bool) def test_get_executable_path(self): """get_executable_path() should return str or None.""" result = SubprocessRunner.get_executable_path() assert result is None or isinstance(result, str) class TestGetRunner: """Tests for the get_runner factory function.""" def test_auto_returns_runner(self): """Auto mode should return some runner if anything is available.""" try: runner = get_runner(backend="auto") assert isinstance(runner, (NgspiceRunner, SubprocessRunner)) except RuntimeError: pytest.skip("No ngspice backend available") def test_invalid_backend(self): """Invalid backend name should raise ValueError.""" with pytest.raises(ValueError, match="Unknown backend"): get_runner(backend="invalid") @pytest.mark.skipif( not NgspiceRunner.detect(), reason="C++ extension not built" ) def test_embedded_backend(self, tmp_workdir): """Explicitly requesting embedded should return NgspiceRunner.""" runner = get_runner(tmp_workdir, backend="embedded") assert isinstance(runner, NgspiceRunner)