Skip to content

Commit b388983

Browse files
authored
test(api): liquid class touch tip properties (#17736)
1 parent fa1bba9 commit b388983

File tree

7 files changed

+286
-60
lines changed

7 files changed

+286
-60
lines changed

api/src/opentrons/protocol_api/_liquid_properties.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def speed(self) -> Optional[float]:
168168

169169
@speed.setter
170170
def speed(self, new_speed: float) -> None:
171-
validated_speed = validation.ensure_positive_float(new_speed)
171+
validated_speed = validation.ensure_greater_than_zero_float(new_speed)
172172
self._speed = validated_speed
173173

174174
def _get_shared_data_params(self) -> Optional[SharedDataTouchTipParams]:

api/src/opentrons/protocol_api/validation.py

+10
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,16 @@ def ensure_positive_float(value: Union[int, float]) -> float:
627627
return float_value
628628

629629

630+
def ensure_greater_than_zero_float(value: Union[int, float]) -> float:
631+
"""Ensure value is a positive and real float value."""
632+
float_value = ensure_float(value)
633+
if isnan(float_value) or isinf(float_value):
634+
raise ValueError("Value must be a defined, non-infinite number.")
635+
if float_value <= 0:
636+
raise ValueError("Value must be a positive float greater than 0.")
637+
return float_value
638+
639+
630640
def ensure_positive_int(value: int) -> int:
631641
"""Ensure value is a positive integer."""
632642
if not isinstance(value, int):

api/tests/opentrons/protocol_api/__init__.py

+58
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
MIN_SUPPORTED_VERSION_FOR_FLEX,
99
)
1010

11+
from hypothesis import strategies as st
12+
1113

1214
def versions_at_or_above(from_version: APIVersion) -> List[APIVersion]:
1315
"""Get a list of versions >= the specified one."""
@@ -129,3 +131,59 @@ def versions_between(
129131
else:
130132
raise ValueError("You must specify one low bound and one high bound")
131133
return [APIVersion(major, minor) for minor in range(start, stop)]
134+
135+
136+
invalid_values = st.one_of(
137+
st.text(min_size=0, max_size=5),
138+
st.dictionaries(keys=st.text(), values=st.integers()),
139+
st.lists(st.integers(min_value=-100, max_value=100), min_size=1, max_size=5),
140+
st.tuples(st.booleans()),
141+
st.binary(min_size=1, max_size=5),
142+
)
143+
144+
boolean_looking_values = st.one_of(
145+
st.just("True"),
146+
st.just("1"),
147+
st.just(1),
148+
st.just("False"),
149+
st.just("0"),
150+
st.just(0),
151+
)
152+
153+
reasonable_floats = st.floats(
154+
min_value=-1000, max_value=1000, allow_infinity=False, allow_nan=False
155+
)
156+
reasonable_ints = st.integers(min_value=-1000, max_value=1000)
157+
158+
reasonable_numbers = st.one_of(reasonable_floats, reasonable_ints)
159+
160+
negative_or_zero_floats = st.floats(
161+
min_value=-1000, max_value=0, allow_infinity=False, allow_nan=False
162+
)
163+
negative_or_zero_ints = st.integers(min_value=-1000, max_value=0)
164+
negative_or_zero_floats_and_ints = st.one_of(
165+
negative_or_zero_floats, negative_or_zero_ints
166+
)
167+
positive_or_zero_ints = st.integers(min_value=0, max_value=1000)
168+
positive_or_zero_floats = st.floats(
169+
min_value=0, max_value=1000, allow_infinity=False, allow_nan=False
170+
)
171+
positive_or_zero_floats_or_ints = st.one_of(
172+
positive_or_zero_floats, positive_or_zero_ints
173+
)
174+
175+
negative_non_zero_floats = st.floats(
176+
min_value=-1000, max_value=-0.0001, allow_infinity=False, allow_nan=False
177+
)
178+
negative_non_zero_ints = st.integers(min_value=-1000, max_value=-1)
179+
negative_non_zero_floats_and_ints = st.one_of(
180+
negative_non_zero_floats, negative_non_zero_ints
181+
)
182+
183+
positive_non_zero_floats = st.floats(
184+
min_value=0.0001, max_value=1000, allow_infinity=False, allow_nan=False
185+
)
186+
positive_non_zero_ints = st.integers(min_value=1, max_value=1000)
187+
positive_non_zero_floats_and_ints = st.one_of(
188+
positive_non_zero_floats, positive_non_zero_ints
189+
)

api/tests/opentrons/protocol_api/test_delay_properties.py

+5-55
Original file line numberDiff line numberDiff line change
@@ -11,61 +11,11 @@
1111
DelayParams,
1212
)
1313

14-
# --------------------------------------------------------------------------------------
15-
# Strategies
16-
# --------------------------------------------------------------------------------------
17-
18-
invalid_values = st.one_of(
19-
st.text(min_size=0, max_size=5),
20-
st.dictionaries(keys=st.text(), values=st.integers()),
21-
st.lists(st.integers(min_value=-100, max_value=100), min_size=1, max_size=5),
22-
st.tuples(st.booleans()),
23-
st.binary(min_size=1, max_size=5),
24-
)
25-
26-
boolean_looking_values = st.one_of(
27-
st.just("True"),
28-
st.just("1"),
29-
st.just(1),
30-
st.just("False"),
31-
st.just("0"),
32-
st.just(0),
33-
)
34-
35-
reasonable_floats = st.floats(
36-
min_value=-1000, max_value=1000, allow_infinity=False, allow_nan=False
37-
)
38-
reasonable_ints = st.integers(min_value=-1000, max_value=1000)
39-
40-
negative_or_zero_floats = st.floats(
41-
min_value=-1000, max_value=0, allow_infinity=False, allow_nan=False
42-
)
43-
negative_or_zero_ints = st.integers(min_value=-1000, max_value=0)
44-
negative_or_zero_floats_and_ints = st.one_of(
45-
negative_or_zero_floats, negative_or_zero_ints
46-
)
47-
positive_or_zero_ints = st.integers(min_value=0, max_value=1000)
48-
positive_or_zero_floats = st.floats(
49-
min_value=0, max_value=1000, allow_infinity=False, allow_nan=False
50-
)
51-
positive_or_zero_floats_or_ints = st.one_of(
52-
positive_or_zero_floats, positive_or_zero_ints
53-
)
54-
55-
negative_non_zero_floats = st.floats(
56-
min_value=-1000, max_value=-0.0001, allow_infinity=False, allow_nan=False
57-
)
58-
negative_non_zero_ints = st.integers(min_value=-1000, max_value=-1)
59-
negative_non_zero_floats_and_ints = st.one_of(
60-
negative_non_zero_floats, negative_non_zero_ints
61-
)
62-
63-
positive_non_zero_floats = st.floats(
64-
min_value=0.0001, max_value=1000, allow_infinity=False, allow_nan=False
65-
)
66-
positive_non_zero_ints = st.integers(min_value=1, max_value=1000)
67-
positive_non_zero_floats_and_ints = st.one_of(
68-
positive_non_zero_floats, positive_non_zero_ints
14+
from . import (
15+
boolean_looking_values,
16+
invalid_values,
17+
negative_non_zero_floats_and_ints,
18+
positive_or_zero_floats_or_ints,
6919
)
7020

7121

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
"""Tests for delay properties in the Opentrons protocol API."""
2+
3+
from pydantic import ValidationError
4+
import pytest
5+
from typing import Any, Union
6+
from hypothesis import given, strategies as st, settings
7+
8+
from opentrons.protocol_api._liquid_properties import _build_touch_tip_properties
9+
from opentrons_shared_data.liquid_classes.liquid_class_definition import (
10+
TouchTipProperties,
11+
LiquidClassTouchTipParams,
12+
)
13+
14+
from . import (
15+
boolean_looking_values,
16+
invalid_values,
17+
positive_non_zero_floats_and_ints,
18+
reasonable_numbers,
19+
negative_or_zero_floats_and_ints,
20+
)
21+
22+
23+
def test_touch_tip_properties_enable_and_disable() -> None:
24+
"""Test enabling and disabling TouchTipProperties."""
25+
tp = _build_touch_tip_properties(
26+
TouchTipProperties(
27+
enable=False,
28+
params=LiquidClassTouchTipParams(zOffset=1, mmToEdge=1, speed=100),
29+
)
30+
)
31+
tp.enabled = True
32+
assert tp.enabled is True
33+
tp.enabled = False
34+
assert tp.enabled is False
35+
36+
37+
def test_touch_tip_properties_none_instantiation_combos() -> None:
38+
"""Test handling of None combinations in TouchTipProperties instantiation."""
39+
with pytest.raises(ValidationError):
40+
_build_touch_tip_properties(
41+
TouchTipProperties(enable=True, params=LiquidClassTouchTipParams(zOffset=None, mmToEdge=None, speed=None)) # type: ignore
42+
)
43+
with pytest.raises(ValidationError):
44+
_build_touch_tip_properties(
45+
TouchTipProperties(enable=None, params=LiquidClassTouchTipParams(zOffset=None, mmToEdge=1, speed=1)) # type: ignore
46+
)
47+
with pytest.raises(ValidationError):
48+
_build_touch_tip_properties(
49+
TouchTipProperties(enable=True, params=LiquidClassTouchTipParams(zOffset=1, mmToEdge=None, speed=1)) # type: ignore
50+
)
51+
with pytest.raises(ValidationError):
52+
_build_touch_tip_properties(
53+
TouchTipProperties(enable=True, params=LiquidClassTouchTipParams(zOffset=1, mmToEdge=1, speed=None)) # type: ignore
54+
)
55+
56+
57+
@given(bad_value=st.one_of(invalid_values, boolean_looking_values))
58+
@settings(deadline=None, max_examples=50)
59+
def test_touch_tip_properties_enabled_bad_values(bad_value: Any) -> None:
60+
"""Test bad values for TouchTipProperties.enabled."""
61+
with pytest.raises(ValidationError):
62+
_build_touch_tip_properties(
63+
TouchTipProperties(
64+
enable=bad_value,
65+
params=LiquidClassTouchTipParams(zOffset=1, mmToEdge=1, speed=1),
66+
)
67+
)
68+
tp = _build_touch_tip_properties(
69+
TouchTipProperties(
70+
enable=False,
71+
params=LiquidClassTouchTipParams(zOffset=1, mmToEdge=1, speed=1),
72+
)
73+
)
74+
with pytest.raises(ValueError):
75+
tp.enabled = bad_value
76+
77+
78+
@given(good_value=reasonable_numbers)
79+
@settings(deadline=None, max_examples=50)
80+
def test_touch_tip_properties_z_offset(good_value: Union[int, float]) -> None:
81+
"""Test valid z_offset."""
82+
_build_touch_tip_properties(
83+
TouchTipProperties(
84+
enable=True,
85+
params=LiquidClassTouchTipParams(zOffset=good_value, mmToEdge=1, speed=10),
86+
)
87+
)
88+
tp = _build_touch_tip_properties(
89+
TouchTipProperties(
90+
enable=False,
91+
params=LiquidClassTouchTipParams(zOffset=0, mmToEdge=1, speed=10),
92+
)
93+
)
94+
tp.z_offset = good_value
95+
assert tp.z_offset == float(good_value)
96+
97+
98+
@given(bad_value=invalid_values)
99+
@settings(deadline=None, max_examples=50)
100+
def test_touch_tip_properties_z_offset_bad_values(bad_value: Any) -> None:
101+
"""Test invalid z_offset values."""
102+
with pytest.raises(ValidationError):
103+
_build_touch_tip_properties(
104+
TouchTipProperties(
105+
enable=True,
106+
params=LiquidClassTouchTipParams(
107+
zOffset=bad_value, mmToEdge=1, speed=10
108+
),
109+
)
110+
)
111+
tp = _build_touch_tip_properties(
112+
TouchTipProperties(
113+
enable=False,
114+
params=LiquidClassTouchTipParams(zOffset=0, mmToEdge=1, speed=10),
115+
)
116+
)
117+
with pytest.raises(ValueError):
118+
tp.z_offset = bad_value
119+
120+
121+
@given(good_value=reasonable_numbers)
122+
@settings(deadline=None, max_examples=50)
123+
def test_touch_tip_properties_mm_to_edge(good_value: Union[int, float]) -> None:
124+
"""Test valid mm_to_edge."""
125+
_build_touch_tip_properties(
126+
TouchTipProperties(
127+
enable=True,
128+
params=LiquidClassTouchTipParams(zOffset=0, mmToEdge=good_value, speed=10),
129+
)
130+
)
131+
tp = _build_touch_tip_properties(
132+
TouchTipProperties(
133+
enable=False,
134+
params=LiquidClassTouchTipParams(zOffset=0, mmToEdge=1, speed=10),
135+
)
136+
)
137+
tp.mm_to_edge = good_value
138+
assert tp.mm_to_edge == float(good_value)
139+
140+
141+
@given(bad_value=invalid_values)
142+
@settings(deadline=None, max_examples=50)
143+
def test_touch_tip_properties_mm_to_edge_bad_values(bad_value: Any) -> None:
144+
"""Test invalid mm_to_edge values."""
145+
with pytest.raises(ValidationError):
146+
_build_touch_tip_properties(
147+
TouchTipProperties(
148+
enable=True,
149+
params=LiquidClassTouchTipParams(
150+
zOffset=bad_value, mmToEdge=1, speed=10
151+
),
152+
)
153+
)
154+
tp = _build_touch_tip_properties(
155+
TouchTipProperties(
156+
enable=True,
157+
params=LiquidClassTouchTipParams(zOffset=0, mmToEdge=1, speed=10),
158+
)
159+
)
160+
with pytest.raises(ValueError):
161+
tp.mm_to_edge = bad_value
162+
163+
164+
@given(good_value=positive_non_zero_floats_and_ints)
165+
@settings(deadline=None, max_examples=50)
166+
def test_touch_tip_properties_speed(good_value: Union[int, float]) -> None:
167+
"""Test valid speed."""
168+
_build_touch_tip_properties(
169+
TouchTipProperties(
170+
enable=True,
171+
params=LiquidClassTouchTipParams(zOffset=0, mmToEdge=1, speed=good_value),
172+
)
173+
)
174+
tp = _build_touch_tip_properties(
175+
TouchTipProperties(
176+
enable=False,
177+
params=LiquidClassTouchTipParams(zOffset=0, mmToEdge=1, speed=10),
178+
)
179+
)
180+
tp.speed = good_value
181+
assert tp.speed == float(good_value)
182+
183+
184+
@given(bad_value=st.one_of(invalid_values, negative_or_zero_floats_and_ints))
185+
@settings(deadline=None, max_examples=50)
186+
def test_touch_tip_properties_speed_bad_values(bad_value: Any) -> None:
187+
"""Test invalid speed values."""
188+
with pytest.raises(ValidationError):
189+
_build_touch_tip_properties(
190+
TouchTipProperties(
191+
enable=True,
192+
params=LiquidClassTouchTipParams(
193+
zOffset=0, mmToEdge=1, speed=bad_value
194+
),
195+
)
196+
)
197+
tp = _build_touch_tip_properties(
198+
TouchTipProperties(
199+
enable=False,
200+
params=LiquidClassTouchTipParams(zOffset=0, mmToEdge=1, speed=10),
201+
)
202+
)
203+
with pytest.raises(ValueError):
204+
tp.speed = bad_value

shared-data/command/schemas/12.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -2465,11 +2465,11 @@
24652465
"speed": {
24662466
"anyOf": [
24672467
{
2468-
"minimum": 0,
2468+
"exclusiveMinimum": 0,
24692469
"type": "integer"
24702470
},
24712471
{
2472-
"minimum": 0.0,
2472+
"exclusiveMinimum": 0.0,
24732473
"type": "number"
24742474
}
24752475
],

0 commit comments

Comments
 (0)