You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.8 KiB
127 lines
4.8 KiB
"""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)
|