Test build_waveform_svg without a simulator or filesystem

11 tests over auto plot-type detection (bode/spectrum/time), missing-signal
and multi-signal-overlay errors, dimension/height defaults, downsampling, and
x-range clipping -- all with hand-built RawFiles, no .raw read or SVG write.
This commit is contained in:
Ryan Malloy 2026-06-20 21:06:35 -06:00
parent b634eb9cd1
commit 35b12e61c4

103
tests/test_plotting.py Normal file
View File

@ -0,0 +1,103 @@
"""Tests for build_waveform_svg -- the plot_waveform core, sans file I/O.
Hand-built RawFiles drive every branch (auto plot-type detection, clipping,
multi-signal, errors) without LTspice or writing an SVG to disk.
"""
import numpy as np
from mcltspice.plotting import build_waveform_svg
from mcltspice.raw_parser import RawFile, Variable
def _ac_raw() -> RawFile:
"""Frequency axis + complex V(out), V(in) -> AC analysis."""
freq = np.logspace(0, 6, 400)
hout = 1.0 / (1.0 + 1j * (freq / 1000.0))
data = np.array([freq.astype(complex), hout, np.ones_like(freq, dtype=complex)])
return RawFile(
title="ac", date="", plotname="AC Analysis", flags=["complex"],
variables=[
Variable(0, "frequency", "frequency"),
Variable(1, "V(out)", "voltage"),
Variable(2, "V(in)", "voltage"),
],
points=len(freq), data=data,
)
def _tran_raw() -> RawFile:
"""Time axis + two real signals -> transient."""
t = np.linspace(0, 0.01, 500)
data = np.array([t, np.sin(2 * np.pi * 1000 * t), np.cos(2 * np.pi * 1000 * t)])
return RawFile(
title="tran", date="", plotname="Transient Analysis", flags=["real"],
variables=[
Variable(0, "time", "time"),
Variable(1, "V(out)", "voltage"),
Variable(2, "V(ref)", "voltage"),
],
points=len(t), data=data,
)
class TestAutoPlotType:
def test_complex_frequency_is_bode(self):
r = build_waveform_svg(_ac_raw(), ["V(out)"])
assert r["plot_type"] == "bode"
assert r["svg"].lstrip().startswith("<") # real SVG markup
def test_real_frequency_is_spectrum(self):
# frequency axis but real signal data -> spectrum
freq = np.logspace(0, 6, 200)
data = np.array([freq, np.abs(1.0 / (1.0 + 1j * freq / 1000.0))])
raw = RawFile(
title="", date="", plotname="AC Analysis", flags=["real"],
variables=[Variable(0, "frequency", "frequency"), Variable(1, "V(out)", "voltage")],
points=len(freq), data=data,
)
assert build_waveform_svg(raw, ["V(out)"])["plot_type"] == "spectrum"
def test_time_axis_is_time(self):
assert build_waveform_svg(_tran_raw(), ["V(out)"])["plot_type"] == "time"
def test_explicit_overrides_auto(self):
assert build_waveform_svg(_tran_raw(), ["V(out)"], plot_type="time")["plot_type"] == "time"
class TestErrorsAndMetadata:
def test_missing_signal(self):
r = build_waveform_svg(_ac_raw(), ["V(ghost)"])
assert "error" in r and "V(ghost)" in r["error"]
assert r["available_signals"] == ["frequency", "V(out)", "V(in)"]
def test_multi_signal_non_time_is_error(self):
r = build_waveform_svg(_ac_raw(), ["V(out)", "V(in)"], plot_type="bode")
assert "error" in r and "time-domain" in r["error"]
def test_dimensions_and_signal_metadata(self):
r = build_waveform_svg(_tran_raw(), ["V(out)"], width=1000, height=600)
assert r["dimensions"] == "1000x600"
assert r["signal"] == "V(out)"
def test_default_height_depends_on_plot_type(self):
assert build_waveform_svg(_ac_raw(), ["V(out)"])["dimensions"] == "800x500" # bode
assert build_waveform_svg(_tran_raw(), ["V(out)"])["dimensions"] == "800x400" # time
class TestSamplingAndMulti:
def test_max_points_downsamples(self):
r = build_waveform_svg(_tran_raw(), ["V(out)"], max_points=100)
assert r["points"] <= 100
def test_x_range_clip_reduces_points(self):
full = build_waveform_svg(_tran_raw(), ["V(out)"], max_points=10000)["points"]
clipped = build_waveform_svg(
_tran_raw(), ["V(out)"], x_min=0.002, x_max=0.004, max_points=10000
)["points"]
assert clipped < full
def test_multi_signal_time_overlay(self):
r = build_waveform_svg(_tran_raw(), ["V(out)", "V(ref)"])
assert r["plot_type"] == "time"
assert r["signal"] == "V(out), V(ref)"