diff --git a/hardware/tektronix_base.py b/hardware/tektronix_base.py index 54f8f3a..54c946f 100755 --- a/hardware/tektronix_base.py +++ b/hardware/tektronix_base.py @@ -292,7 +292,7 @@ class TektronixOscilloscopeBase: # Validate coupling valid = False - for variants in self.CHANNEL_COUPLING.items(): + for variants in self.CHANNEL_COUPLING.values(): if coupling_upper in [v.upper() for v in variants]: valid = True break diff --git a/tests/test_tektronix_base.py b/tests/test_tektronix_base.py new file mode 100644 index 0000000..16ba18d --- /dev/null +++ b/tests/test_tektronix_base.py @@ -0,0 +1,71 @@ +"""TektronixOscilloscopeBase: the SCPI setters that validate their input. + +These run against the real class with only the socket replaced. The scan +tests use FakeScope, which stubs the setters out entirely — so a setter +could raise on every call and nothing in the suite would notice, which is +what happened: set_channel_coupling() iterated CHANNEL_COUPLING.items() +instead of .values(), called .upper() on the list half of each pair, and +raised AttributeError for any coupling at all. configure_channels() runs +on connect, so the scope could not be connected to. +""" +import pytest + +from core.scope_sras import SRAS_CHANNELS, configure_channels +from hardware.tektronix_base import TektronixOscilloscopeBase + + +class RecordingScope(TektronixOscilloscopeBase): + """The real instrument class with the wire replaced by a list.""" + + def __init__(self): + super().__init__(resource_name="192.0.2.1") + self._connected = True + self.written: list[str] = [] + + def write(self, command): + self.written.append(command) + + +@pytest.fixture +def scope(): + return RecordingScope() + + +def test_connect_time_channel_setup_reaches_the_wire(scope): + """The regression: every command configure_channels() sends must go out.""" + configure_channels(scope) + + for ch, profile in SRAS_CHANNELS.items(): + assert f"SELect:CH{ch} ON" in scope.written + assert f"CH{ch}:COUPling {profile.coupling}" in scope.written + assert f"CH{ch}:TERmination {profile.termination_ohm}" in scope.written + assert f"CH{ch}:SCAle {profile.scale_v_div}" in scope.written + + +@pytest.mark.parametrize("coupling", ["DC", "AC", "dc", "ac"]) +def test_channel_coupling_accepts_both_modes_in_any_case(scope, coupling): + scope.set_channel_coupling(1, coupling) + assert scope.written == [f"CH1:COUPling {coupling}"] + + +def test_channel_coupling_rejects_an_unknown_mode(scope): + """Rejection has to be the documented ValueError, not an AttributeError + raised while building the list of valid options.""" + with pytest.raises(ValueError, match="Invalid coupling mode"): + scope.set_channel_coupling(1, "GND") + assert scope.written == [] + + +# The other setters share this shape; a table keeps them honest together. +@pytest.mark.parametrize("setter,good,bad,sent", [ + ("set_acquire_mode", "AVERAGE", "SMOOTH", "ACQuire:MODe AVERAGE"), + ("set_acquire_mode", "SAM", "SMOOTH", "ACQuire:MODe SAM"), + ("set_trigger_slope", "RISE", "SIDEWAYS", "TRIGger:A:EDGE:SLOpe RISE"), + ("set_trigger_mode", "NORMAL", "SOMETIMES", "TRIGger:A:MODe NORMAL"), +]) +def test_validated_setters_take_valid_values_and_reject_the_rest( + scope, setter, good, bad, sent): + getattr(scope, setter)(good) + assert scope.written == [sent] + with pytest.raises(ValueError): + getattr(scope, setter)(bad)