Merge fix-scope-channel-coupling: the scope could not be connected to
set_channel_coupling() looped over CHANNEL_COUPLING.items(), so it called .upper() on the list half of each pair and raised "'list' object has no attribute 'upper'" before looking at its argument. configure_channels() sets the coupling on every channel as part of connecting, so no IP would work. Fixed to .values(), with a test that drives the real instrument class rather than the FakeScope that stubbed the setters out.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user