mcltspice/tests/test_new_templates.py
Ryan Malloy cf8394fa6f Rename mcp-ltspice -> mcltspice, remove stdout banner
Rename package from mcp-ltspice/mcp_ltspice to mcltspice throughout:
source directory, imports, pyproject.toml, tests, and README.

Remove startup banner prints from main() since FastMCP handles
its own banner and stdout is the MCP JSON-RPC transport.

Point repo URL at git.supported.systems/MCP/mcltspice.
2026-02-12 22:53:16 -07:00

448 lines
13 KiB
Python

"""Tests for the 5 new circuit templates (netlist + asc generator)."""
import pytest
from mcltspice.asc_generator import (
AscSchematic,
generate_boost_converter,
generate_current_mirror,
generate_instrumentation_amp,
generate_sallen_key_lowpass,
generate_transimpedance_amp,
)
from mcltspice.netlist import (
Netlist,
boost_converter,
current_mirror,
instrumentation_amplifier,
sallen_key_lowpass,
transimpedance_amplifier,
)
# ---------------------------------------------------------------------------
# Netlist template tests
# ---------------------------------------------------------------------------
class TestSallenKeyLowpassNetlist:
def test_returns_netlist(self):
n = sallen_key_lowpass()
assert isinstance(n, Netlist)
def test_component_count(self):
n = sallen_key_lowpass()
# V1, Vpos, Vneg, R1, R2, C1, C2, X1 = 8 components
assert len(n.components) == 8
def test_render_contains_key_components(self):
text = sallen_key_lowpass().render()
assert "R1" in text
assert "R2" in text
assert "C1" in text
assert "C2" in text
assert "X1" in text
assert "LT1001" in text
assert ".ac" in text
def test_custom_params(self):
n = sallen_key_lowpass(r1="4.7k", r2="4.7k", c1="22n", c2="22n")
text = n.render()
assert "4.7k" in text
assert "22n" in text
def test_has_backanno_and_end(self):
text = sallen_key_lowpass().render()
assert ".backanno" in text
assert ".end" in text
class TestBoostConverterNetlist:
def test_returns_netlist(self):
n = boost_converter()
assert isinstance(n, Netlist)
def test_component_count(self):
n = boost_converter()
# Vin, Vgate, L1, M1, D1, Cout, Rload = 7 components
assert len(n.components) == 7
def test_render_contains_key_components(self):
text = boost_converter().render()
assert "L1" in text
assert "M1" in text
assert "D1" in text
assert "Cout" in text
assert "Rload" in text
assert "PULSE(" in text
assert ".tran" in text
def test_custom_params(self):
n = boost_converter(ind="22u", r_load="100", v_in="3.3", duty_cycle=0.6)
text = n.render()
assert "22u" in text
assert "100" in text
assert "3.3" in text
def test_has_backanno_and_end(self):
text = boost_converter().render()
assert ".backanno" in text
assert ".end" in text
class TestInstrumentationAmplifierNetlist:
def test_returns_netlist(self):
n = instrumentation_amplifier()
assert isinstance(n, Netlist)
def test_component_count(self):
n = instrumentation_amplifier()
# V1, V2, Vpos, Vneg, X1, X2, R1, R1b, Rgain, X3, R2, R3, R2b, R3b = 14
assert len(n.components) == 14
def test_render_contains_key_components(self):
text = instrumentation_amplifier().render()
assert "X1" in text
assert "X2" in text
assert "X3" in text
assert "Rgain" in text
assert "LT1001" in text
assert ".ac" in text
def test_custom_params(self):
n = instrumentation_amplifier(r1="20k", r_gain="1k")
text = n.render()
assert "20k" in text
assert "1k" in text
def test_has_backanno_and_end(self):
text = instrumentation_amplifier().render()
assert ".backanno" in text
assert ".end" in text
class TestCurrentMirrorNetlist:
def test_returns_netlist(self):
n = current_mirror()
assert isinstance(n, Netlist)
def test_component_count(self):
n = current_mirror()
# Vcc, Rref, Rload, Q1, Q2 = 5 components
assert len(n.components) == 5
def test_render_contains_key_components(self):
text = current_mirror().render()
assert "Q1" in text
assert "Q2" in text
assert "Rref" in text
assert "Rload" in text
assert "2N2222" in text
assert ".op" in text
assert ".tran" in text
def test_custom_params(self):
n = current_mirror(r_ref="4.7k", r_load="2.2k", vcc="5")
text = n.render()
assert "4.7k" in text
assert "2.2k" in text
assert "5" in text
def test_has_backanno_and_end(self):
text = current_mirror().render()
assert ".backanno" in text
assert ".end" in text
class TestTransimpedanceAmplifierNetlist:
def test_returns_netlist(self):
n = transimpedance_amplifier()
assert isinstance(n, Netlist)
def test_component_count(self):
n = transimpedance_amplifier()
# I1, Vpos, Vneg, Rf, Cf, X1 = 6 components
assert len(n.components) == 6
def test_render_contains_key_components(self):
text = transimpedance_amplifier().render()
assert "I1" in text
assert "Rf" in text
assert "Cf" in text
assert "X1" in text
assert "LT1001" in text
assert ".ac" in text
def test_custom_params(self):
n = transimpedance_amplifier(rf="1Meg", cf="0.5p", i_source="10u")
text = n.render()
assert "1Meg" in text
assert "0.5p" in text
assert "10u" in text
def test_has_backanno_and_end(self):
text = transimpedance_amplifier().render()
assert ".backanno" in text
assert ".end" in text
# ---------------------------------------------------------------------------
# ASC generator template tests
# ---------------------------------------------------------------------------
class TestSallenKeyLowpassAsc:
def test_returns_schematic(self):
sch = generate_sallen_key_lowpass()
assert isinstance(sch, AscSchematic)
def test_render_valid(self):
text = generate_sallen_key_lowpass().render()
assert text.startswith("Version 4\n")
assert "SHEET" in text
assert len(text) > 100
def test_contains_expected_symbols(self):
text = generate_sallen_key_lowpass().render()
assert "SYMBOL res" in text
assert "SYMBOL cap" in text
assert "SYMBOL OpAmps/UniversalOpamp2" in text
assert "SYMBOL voltage" in text
def test_custom_params(self):
text = generate_sallen_key_lowpass(r1="4.7k", c1="22n").render()
assert "4.7k" in text
assert "22n" in text
def test_has_simulation_directive(self):
text = generate_sallen_key_lowpass().render()
assert ".ac" in text
class TestBoostConverterAsc:
def test_returns_schematic(self):
sch = generate_boost_converter()
assert isinstance(sch, AscSchematic)
def test_render_valid(self):
text = generate_boost_converter().render()
assert text.startswith("Version 4\n")
assert "SHEET" in text
assert len(text) > 100
def test_contains_expected_symbols(self):
text = generate_boost_converter().render()
assert "SYMBOL ind" in text
assert "SYMBOL nmos" in text
assert "SYMBOL diode" in text
assert "SYMBOL cap" in text
assert "SYMBOL res" in text
def test_custom_params(self):
text = generate_boost_converter(ind="22u", r_load="100").render()
assert "22u" in text
assert "100" in text
def test_has_simulation_directive(self):
text = generate_boost_converter().render()
assert ".tran" in text
class TestInstrumentationAmpAsc:
def test_returns_schematic(self):
sch = generate_instrumentation_amp()
assert isinstance(sch, AscSchematic)
def test_render_valid(self):
text = generate_instrumentation_amp().render()
assert text.startswith("Version 4\n")
assert "SHEET" in text
assert len(text) > 100
def test_contains_expected_symbols(self):
text = generate_instrumentation_amp().render()
# Should have 3 opamps
assert text.count("SYMBOL OpAmps/UniversalOpamp2") == 3
# Should have multiple resistors
assert text.count("SYMBOL res") >= 7
def test_custom_params(self):
text = generate_instrumentation_amp(r1="20k", r_gain="1k").render()
assert "20k" in text
assert "1k" in text
def test_has_simulation_directive(self):
text = generate_instrumentation_amp().render()
assert ".ac" in text
class TestCurrentMirrorAsc:
def test_returns_schematic(self):
sch = generate_current_mirror()
assert isinstance(sch, AscSchematic)
def test_render_valid(self):
text = generate_current_mirror().render()
assert text.startswith("Version 4\n")
assert "SHEET" in text
assert len(text) > 100
def test_contains_expected_symbols(self):
text = generate_current_mirror().render()
# Should have 2 NPN transistors
assert text.count("SYMBOL npn") == 2
assert "SYMBOL res" in text
assert "SYMBOL voltage" in text
def test_custom_params(self):
text = generate_current_mirror(r_ref="4.7k", r_load="2.2k").render()
assert "4.7k" in text
assert "2.2k" in text
def test_has_simulation_directive(self):
text = generate_current_mirror().render()
assert ".op" in text
assert ".tran" in text
class TestTransimpedanceAmpAsc:
def test_returns_schematic(self):
sch = generate_transimpedance_amp()
assert isinstance(sch, AscSchematic)
def test_render_valid(self):
text = generate_transimpedance_amp().render()
assert text.startswith("Version 4\n")
assert "SHEET" in text
assert len(text) > 100
def test_contains_expected_symbols(self):
text = generate_transimpedance_amp().render()
assert "SYMBOL OpAmps/UniversalOpamp2" in text
assert "SYMBOL res" in text
assert "SYMBOL cap" in text
def test_custom_params(self):
text = generate_transimpedance_amp(rf="1Meg", cf="0.5p").render()
assert "1Meg" in text
assert "0.5p" in text
def test_has_simulation_directive(self):
text = generate_transimpedance_amp().render()
assert ".ac" in text
# ---------------------------------------------------------------------------
# Parametrized cross-cutting tests for all 5 new netlist templates
# ---------------------------------------------------------------------------
class TestNewNetlistTemplatesCommon:
@pytest.mark.parametrize(
"factory",
[
sallen_key_lowpass,
boost_converter,
instrumentation_amplifier,
current_mirror,
transimpedance_amplifier,
],
)
def test_returns_netlist(self, factory):
assert isinstance(factory(), Netlist)
@pytest.mark.parametrize(
"factory",
[
sallen_key_lowpass,
boost_converter,
instrumentation_amplifier,
current_mirror,
transimpedance_amplifier,
],
)
def test_has_backanno_and_end(self, factory):
text = factory().render()
assert ".backanno" in text
assert ".end" in text
@pytest.mark.parametrize(
"factory",
[
sallen_key_lowpass,
boost_converter,
instrumentation_amplifier,
current_mirror,
transimpedance_amplifier,
],
)
def test_has_components(self, factory):
n = factory()
assert len(n.components) > 0
@pytest.mark.parametrize(
"factory",
[
sallen_key_lowpass,
boost_converter,
instrumentation_amplifier,
current_mirror,
transimpedance_amplifier,
],
)
def test_has_sim_directive(self, factory):
text = factory().render()
sim_types = [".tran", ".ac", ".dc", ".op", ".noise", ".tf"]
assert any(sim in text.lower() for sim in sim_types), (
f"No simulation directive found in {factory.__name__}"
)
# ---------------------------------------------------------------------------
# Parametrized cross-cutting tests for all 5 new ASC templates
# ---------------------------------------------------------------------------
class TestNewAscTemplatesCommon:
@pytest.mark.parametrize(
"factory",
[
generate_sallen_key_lowpass,
generate_boost_converter,
generate_instrumentation_amp,
generate_current_mirror,
generate_transimpedance_amp,
],
)
def test_returns_schematic(self, factory):
assert isinstance(factory(), AscSchematic)
@pytest.mark.parametrize(
"factory",
[
generate_sallen_key_lowpass,
generate_boost_converter,
generate_instrumentation_amp,
generate_current_mirror,
generate_transimpedance_amp,
],
)
def test_render_nonempty(self, factory):
text = factory().render()
assert len(text) > 50
assert "SYMBOL" in text
@pytest.mark.parametrize(
"factory",
[
generate_sallen_key_lowpass,
generate_boost_converter,
generate_instrumentation_amp,
generate_current_mirror,
generate_transimpedance_amp,
],
)
def test_has_version_and_sheet(self, factory):
text = factory().render()
assert "Version 4" in text
assert "SHEET" in text