Skip to content

Commit 482a1fa

Browse files
jenshnielsenCopilot
andcommitted
Add tests for parameters, instrument core, channels, and station
Add 217 new tests across 7 test files targeting the largest remaining coverage gaps in non-driver modules. Parameters tests (71 tests): - tests/parameter/test_command_extended.py (17 tests): all call_by_str and call_cmd method combinations, arg count validation, NoCommandError - tests/parameter/test_function_extended.py (16 tests): validation, name properties, parsers, callable/string cmd, get_attrs - tests/parameter/test_grouped_parameter_extended.py (22 tests): DelegateGroup set/get with custom setter/getter, source_parameters, GroupedParameter repr and properties - tests/parameter/test_combined_parameter_extended.py (16 tests): combine(), aggregator, iter/len, snapshot_base, units deprecation Instrument core tests (146 tests): - tests/test_instrument_extended.py: write_raw/ask_raw NotImplementedError, close_all, find_instrument, exist/is_valid, repr, label, add_function, add_submodule, get_component, print_readable_snapshot, invalidate_cache, parent/ancestors/root_instrument, find_or_create_instrument - tests/test_channel_extended.py: InstrumentModule proxy methods, ChannelTuple operations (reversed/contains/add/index/count/get_by_name), ChannelList mutations and lock behavior, ChannelTupleValidator - tests/test_station_extended.py: snapshot_base, add/remove/get component, close_all_registered_instruments, Station.default handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 96f966c commit 482a1fa

7 files changed

Lines changed: 2483 additions & 0 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""Extended tests for qcodes.parameters.combined_parameter module."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
7+
import numpy as np
8+
import pytest
9+
10+
from qcodes.parameters import Parameter
11+
from qcodes.parameters.combined_parameter import CombinedParameter, combine
12+
13+
14+
@pytest.fixture()
15+
def two_params() -> list[Parameter]:
16+
return [
17+
Parameter("x", set_cmd=None, get_cmd=None),
18+
Parameter("y", set_cmd=None, get_cmd=None),
19+
]
20+
21+
22+
class TestCombineFunction:
23+
def test_combine_creates_combined_parameter(
24+
self, two_params: list[Parameter]
25+
) -> None:
26+
"""combine() convenience function returns CombinedParameter."""
27+
cp = combine(*two_params, name="xy")
28+
assert isinstance(cp, CombinedParameter)
29+
assert cp.dimensionality == 2
30+
31+
def test_combine_with_label_and_unit(self, two_params: list[Parameter]) -> None:
32+
"""combine() passes label and unit through."""
33+
cp = combine(*two_params, name="xy", label="X and Y", unit="V")
34+
assert cp.parameter.label == "X and Y"
35+
assert cp.parameter.unit == "V"
36+
37+
def test_combine_with_aggregator(self, two_params: list[Parameter]) -> None:
38+
"""combine() passes aggregator through."""
39+
cp = combine(*two_params, name="xy", aggregator=sum)
40+
assert hasattr(cp, "aggregate")
41+
42+
43+
class TestCombinedParameter:
44+
def test_set_calls_parameter_sets(self, two_params: list[Parameter]) -> None:
45+
"""set() sets each parameter in order."""
46+
cp = CombinedParameter(two_params, name="xy")
47+
swept = cp.sweep(np.array([[1.0, 2.0], [3.0, 4.0]]))
48+
swept.set(0)
49+
assert two_params[0]() == 1.0
50+
assert two_params[1]() == 2.0
51+
swept.set(1)
52+
assert two_params[0]() == 3.0
53+
assert two_params[1]() == 4.0
54+
55+
def test_aggregate_with_aggregator(self, two_params: list[Parameter]) -> None:
56+
"""_aggregate calls the aggregator function."""
57+
58+
def my_agg(*vals: int) -> int:
59+
return sum(vals)
60+
61+
cp = CombinedParameter(two_params, name="xy", aggregator=my_agg)
62+
result = cp._aggregate(1, 2, 3)
63+
assert result == 6
64+
65+
def test_aggregate_without_aggregator(self, two_params: list[Parameter]) -> None:
66+
"""Without aggregator, _aggregate is not set as 'aggregate' attr."""
67+
cp = CombinedParameter(two_params, name="xy")
68+
assert not hasattr(cp, "aggregate")
69+
70+
def test_iter(self, two_params: list[Parameter]) -> None:
71+
"""__iter__ iterates over setpoint indices."""
72+
cp = CombinedParameter(two_params, name="xy")
73+
swept = cp.sweep(np.array([[1, 2], [3, 4], [5, 6]]))
74+
indices = list(swept)
75+
assert indices == [0, 1, 2]
76+
77+
def test_len(self, two_params: list[Parameter]) -> None:
78+
"""__len__ returns number of setpoints."""
79+
cp = CombinedParameter(two_params, name="xy")
80+
swept = cp.sweep(np.array([[1, 2], [3, 4]]))
81+
assert len(swept) == 2
82+
83+
def test_len_no_setpoints(self, two_params: list[Parameter]) -> None:
84+
"""__len__ returns 0 when no setpoints."""
85+
cp = CombinedParameter(two_params, name="xy")
86+
assert len(cp) == 0
87+
88+
def test_snapshot_base(self, two_params: list[Parameter]) -> None:
89+
"""snapshot_base returns dict with expected keys."""
90+
cp = CombinedParameter(two_params, name="xy", label="combined", unit="mV")
91+
snap = cp.snapshot_base()
92+
assert snap["label"] == "combined"
93+
assert snap["unit"] == "mV"
94+
assert snap["full_name"] == "xy"
95+
assert "__class__" in snap
96+
assert "aggregator" in snap
97+
98+
def test_snapshot_base_with_aggregator(self, two_params: list[Parameter]) -> None:
99+
"""snapshot_base includes aggregator repr."""
100+
cp = CombinedParameter(two_params, name="xy", aggregator=sum)
101+
snap = cp.snapshot_base()
102+
assert "sum" in snap["aggregator"]
103+
104+
def test_units_deprecated(
105+
self, two_params: list[Parameter], caplog: pytest.LogCaptureFixture
106+
) -> None:
107+
"""Passing units= triggers a deprecation warning log."""
108+
with caplog.at_level(logging.WARNING):
109+
cp = CombinedParameter(two_params, name="xy", units="mV")
110+
assert any("`units` is deprecated" in msg for msg in caplog.messages)
111+
assert cp.parameter.unit == "mV"
112+
113+
def test_units_deprecated_unit_takes_precedence(
114+
self, two_params: list[Parameter], caplog: pytest.LogCaptureFixture
115+
) -> None:
116+
"""When both unit and units are given, unit takes precedence."""
117+
with caplog.at_level(logging.WARNING):
118+
cp = CombinedParameter(two_params, name="xy", unit="V", units="mV")
119+
assert cp.parameter.unit == "V"
120+
121+
def test_invalid_name_raises(self, two_params: list[Parameter]) -> None:
122+
"""Invalid parameter name raises ValueError."""
123+
with pytest.raises(ValueError, match="valid identifier"):
124+
CombinedParameter(two_params, name="invalid name")
125+
126+
def test_sweep_multiple_arrays(self, two_params: list[Parameter]) -> None:
127+
"""sweep() with multiple 1D arrays."""
128+
cp = CombinedParameter(two_params, name="xy")
129+
swept = cp.sweep(np.array([1, 2, 3]), np.array([4, 5, 6]))
130+
assert len(swept) == 3
131+
swept.set(0)
132+
assert two_params[0]() == 1
133+
assert two_params[1]() == 4
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
"""Extended tests for qcodes.parameters.command.Command covering all call_by_* methods."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from qcodes.parameters.command import Command, NoCommandError
8+
9+
10+
def test_call_by_str_no_parsers() -> None:
11+
"""String cmd + exec_str, no parsers -> call_by_str."""
12+
results: list[str] = []
13+
14+
def exec_fn(cmd_str: str) -> str:
15+
results.append(cmd_str)
16+
return cmd_str
17+
18+
cmd = Command(arg_count=1, cmd="SET {}", exec_str=exec_fn)
19+
result = cmd(42)
20+
assert result == "SET 42"
21+
assert results == ["SET 42"]
22+
23+
24+
def test_call_by_str_zero_args() -> None:
25+
"""String cmd with 0 args -> call_by_str with no formatting."""
26+
27+
def exec_fn(cmd_str: str) -> str:
28+
return cmd_str
29+
30+
cmd = Command(arg_count=0, cmd="*RST", exec_str=exec_fn)
31+
assert cmd() == "*RST"
32+
33+
34+
def test_call_by_str_parsed_out() -> None:
35+
"""String cmd + output_parser -> call_by_str_parsed_out."""
36+
37+
def exec_fn(cmd_str: str) -> str:
38+
return cmd_str
39+
40+
cmd = Command(
41+
arg_count=1,
42+
cmd="READ {}",
43+
exec_str=exec_fn,
44+
output_parser=lambda x: x.upper(),
45+
)
46+
result = cmd("ch1")
47+
assert result == "READ CH1"
48+
49+
50+
def test_call_by_str_parsed_in() -> None:
51+
"""String cmd + single input_parser -> call_by_str_parsed_in."""
52+
53+
def exec_fn(cmd_str: str) -> str:
54+
return cmd_str
55+
56+
cmd = Command(
57+
arg_count=1,
58+
cmd="SET {}",
59+
exec_str=exec_fn,
60+
input_parser=lambda x: x * 2,
61+
)
62+
result = cmd(5)
63+
assert result == "SET 10"
64+
65+
66+
def test_call_by_str_parsed_in_out() -> None:
67+
"""String cmd + input_parser + output_parser -> call_by_str_parsed_in_out."""
68+
69+
def exec_fn(cmd_str: str) -> str:
70+
return cmd_str
71+
72+
cmd = Command(
73+
arg_count=1,
74+
cmd="MEAS {}",
75+
exec_str=exec_fn,
76+
input_parser=lambda x: x + 1,
77+
output_parser=lambda x: f"result:{x}",
78+
)
79+
result = cmd(9)
80+
assert result == "result:MEAS 10"
81+
82+
83+
def test_call_by_str_parsed_in2() -> None:
84+
"""String cmd + multi-arg input_parser -> call_by_str_parsed_in2."""
85+
86+
def exec_fn(cmd_str: str) -> str:
87+
return cmd_str
88+
89+
def multi_parser(a: int, b: int) -> tuple[int, int]:
90+
return (a * 10, b * 10)
91+
92+
cmd = Command(
93+
arg_count=2,
94+
cmd="SET {} {}",
95+
exec_str=exec_fn,
96+
input_parser=multi_parser,
97+
)
98+
result = cmd(3, 4)
99+
assert result == "SET 30 40"
100+
101+
102+
def test_call_by_str_parsed_in2_out() -> None:
103+
"""String cmd + multi-arg input_parser + output_parser."""
104+
105+
def exec_fn(cmd_str: str) -> str:
106+
return cmd_str
107+
108+
def multi_parser(a: int, b: int) -> tuple[int, int]:
109+
return (a + 1, b + 1)
110+
111+
cmd = Command(
112+
arg_count=2,
113+
cmd="CMD {} {}",
114+
exec_str=exec_fn,
115+
input_parser=multi_parser,
116+
output_parser=lambda x: x.replace("CMD", "OUT"),
117+
)
118+
result = cmd(0, 1)
119+
assert result == "OUT 1 2"
120+
121+
122+
def test_call_cmd_no_parsers() -> None:
123+
"""Callable cmd, no parsers -> direct call."""
124+
125+
def my_func(a: int) -> int:
126+
return a * 3
127+
128+
cmd = Command(arg_count=1, cmd=my_func)
129+
assert cmd(7) == 21
130+
131+
132+
def test_call_cmd_parsed_out() -> None:
133+
"""Callable cmd + output_parser -> call_cmd_parsed_out."""
134+
135+
def my_func(a: int) -> int:
136+
return a + 1
137+
138+
cmd = Command(
139+
arg_count=1,
140+
cmd=my_func,
141+
output_parser=lambda x: x * 100,
142+
)
143+
assert cmd(5) == 600
144+
145+
146+
def test_call_cmd_parsed_in() -> None:
147+
"""Callable cmd + single input_parser -> call_cmd_parsed_in."""
148+
149+
def my_func(a: int) -> int:
150+
return a
151+
152+
cmd = Command(
153+
arg_count=1,
154+
cmd=my_func,
155+
input_parser=lambda x: x + 10,
156+
)
157+
assert cmd(5) == 15
158+
159+
160+
def test_call_cmd_parsed_in_out() -> None:
161+
"""Callable cmd + input_parser + output_parser -> call_cmd_parsed_in_out."""
162+
163+
def my_func(a: int) -> int:
164+
return a * 2
165+
166+
cmd = Command(
167+
arg_count=1,
168+
cmd=my_func,
169+
input_parser=lambda x: x + 1,
170+
output_parser=lambda x: x + 100,
171+
)
172+
# input_parser(3) = 4, my_func(4) = 8, output_parser(8) = 108
173+
assert cmd(3) == 108
174+
175+
176+
def test_call_cmd_parsed_in2() -> None:
177+
"""Callable cmd + multi-arg input_parser -> call_cmd_parsed_in2."""
178+
179+
def my_func(a: int, b: int) -> int:
180+
return a + b
181+
182+
def multi_parser(a: int, b: int) -> tuple[int, int]:
183+
return (a * 10, b * 10)
184+
185+
cmd = Command(
186+
arg_count=2,
187+
cmd=my_func,
188+
input_parser=multi_parser,
189+
)
190+
assert cmd(3, 4) == 70
191+
192+
193+
def test_call_cmd_parsed_in2_out() -> None:
194+
"""Callable cmd + multi-arg input_parser + output_parser."""
195+
196+
def my_func(a: int, b: int) -> int:
197+
return a + b
198+
199+
def multi_parser(a: int, b: int) -> tuple[int, int]:
200+
return (a * 2, b * 3)
201+
202+
cmd = Command(
203+
arg_count=2,
204+
cmd=my_func,
205+
input_parser=multi_parser,
206+
output_parser=lambda x: x * -1,
207+
)
208+
# multi_parser(5, 10) = (10, 30), my_func(10, 30) = 40, output_parser(40) = -40
209+
assert cmd(5, 10) == -40
210+
211+
212+
def test_wrong_arg_count_raises_type_error() -> None:
213+
"""Calling with wrong number of args raises TypeError."""
214+
215+
def my_func(a: int) -> int:
216+
return a
217+
218+
cmd = Command(arg_count=1, cmd=my_func)
219+
220+
with pytest.raises(TypeError, match="command takes exactly 1 args"):
221+
cmd()
222+
223+
with pytest.raises(TypeError, match="command takes exactly 1 args"):
224+
cmd(1, 2)
225+
226+
227+
def test_no_command_error_when_no_cmd() -> None:
228+
"""NoCommandError raised when no cmd and no no_cmd_function."""
229+
with pytest.raises(NoCommandError, match="no ``cmd`` provided"):
230+
Command(arg_count=0, cmd=None)
231+
232+
233+
def test_no_cmd_with_no_cmd_function() -> None:
234+
"""no_cmd_function is used as fallback when cmd is None."""
235+
236+
def fallback() -> str:
237+
return "fallback_called"
238+
239+
cmd = Command(arg_count=0, cmd=None, no_cmd_function=fallback)
240+
assert cmd() == "fallback_called"
241+
242+
243+
def test_str_cmd_without_exec_str_raises() -> None:
244+
"""String cmd with no exec_str raises TypeError."""
245+
with pytest.raises(TypeError, match="exec_str cannot be None"):
246+
Command(arg_count=0, cmd="*RST", exec_str=None)

0 commit comments

Comments
 (0)