Scope: set_channel_coupling() iterated items() and raised on every call
Connecting to the oscilloscope failed with "'list' object has no attribute
'upper'" whatever the IP was. configure_channels() runs on connect and
sets the coupling on all four channels; set_channel_coupling() looped over
CHANNEL_COUPLING.items(), so `variants` was the whole ('AC', ['AC']) pair
and the comprehension called .upper() on the list half. It raised before
looking at the value it was given, so no coupling could ever be accepted.
The tuple unpacking was dropped from `for long_form, variants in ...items()`
in 709dc529 without changing .items() to .values(); the four sibling
validators (acquire mode, trigger slope, trigger mode, data encoding) all
use .values().
The suite missed it because tests/fakes.py FakeScope replaces these setters
with recording stubs, so nothing exercised the real class. The new test
drives TektronixOscilloscopeBase itself with only the socket replaced,
covering the connect-time path and the other validated setters alongside.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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