Skip to content

Commit a9c1a6d

Browse files
authored
test(api): tighten liquid class mix properties (#17807)
1 parent e76cde2 commit a9c1a6d

File tree

6 files changed

+121
-5
lines changed

6 files changed

+121
-5
lines changed

api/src/opentrons/protocol_api/_liquid_properties.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def volume(self) -> Optional[float]:
226226

227227
@volume.setter
228228
def volume(self, new_volume: float) -> None:
229-
validated_volume = validation.ensure_positive_float(new_volume)
229+
validated_volume = validation.ensure_greater_than_zero_float(new_volume)
230230
self._volume = validated_volume
231231

232232
def _get_shared_data_params(self) -> Optional[SharedDataMixParams]:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Tests for mix 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_mix_properties
9+
from opentrons_shared_data.liquid_classes.liquid_class_definition import (
10+
MixProperties,
11+
MixParams,
12+
)
13+
14+
from . import (
15+
boolean_looking_values,
16+
invalid_values,
17+
positive_non_zero_floats_and_ints,
18+
negative_or_zero_floats_and_ints,
19+
)
20+
21+
22+
def test_mix_properties_enable_and_disable() -> None:
23+
"""Test enabling and disabling for boolean-only mix properties."""
24+
mp = _build_mix_properties(
25+
MixProperties(enable=False, params=MixParams(repetitions=2, volume=10))
26+
)
27+
mp.enabled = True
28+
assert mp.enabled is True
29+
mp.enabled = False
30+
assert mp.enabled is False
31+
32+
33+
def test_mix_properties_none_instantiation_combos() -> None:
34+
"""Test handling of None combos in mix properties instantiation."""
35+
with pytest.raises(ValidationError):
36+
_build_mix_properties(MixProperties(enable=True, params=None))
37+
with pytest.raises(ValidationError):
38+
_build_mix_properties(MixProperties(enable=None, params=MixParams(repetitions=2, volume=10))) # type: ignore
39+
_build_mix_properties(MixProperties(enable=False, params=None))
40+
41+
42+
@given(bad_value=st.one_of(invalid_values, boolean_looking_values))
43+
@settings(deadline=None, max_examples=50)
44+
def test_mix_properties_enabled_bad_values(bad_value: Any) -> None:
45+
"""Test bad values for MixProperties.enabled."""
46+
with pytest.raises(ValidationError):
47+
_build_mix_properties(
48+
MixProperties(enable=bad_value, params=MixParams(repetitions=1, volume=5))
49+
)
50+
mp = _build_mix_properties(
51+
MixProperties(enable=True, params=MixParams(repetitions=2, volume=10))
52+
)
53+
with pytest.raises(ValueError):
54+
mp.enabled = bad_value
55+
56+
57+
@given(good_volume=positive_non_zero_floats_and_ints)
58+
@settings(deadline=None, max_examples=50)
59+
def test_mix_properties_volume_good_values(good_volume: Union[int, float]) -> None:
60+
"""Test valid float/int > 0 for MixProperties volume."""
61+
mp = _build_mix_properties(
62+
MixProperties(enable=True, params=MixParams(repetitions=2, volume=5))
63+
)
64+
mp.volume = good_volume
65+
assert mp.volume == float(good_volume)
66+
67+
68+
@given(bad_volume=st.one_of(negative_or_zero_floats_and_ints, invalid_values))
69+
@settings(deadline=None, max_examples=50)
70+
def test_mix_properties_volume_bad_values(bad_volume: Any) -> None:
71+
"""Test invalid float/int <= 0 for MixProperties volume."""
72+
with pytest.raises(ValidationError):
73+
_build_mix_properties(
74+
MixProperties(
75+
enable=True, params=MixParams(repetitions=2, volume=bad_volume)
76+
)
77+
)
78+
mp = _build_mix_properties(
79+
MixProperties(enable=True, params=MixParams(repetitions=2, volume=5))
80+
)
81+
with pytest.raises(ValueError):
82+
mp.volume = bad_volume
83+
84+
85+
@given(good_reps=st.integers(min_value=0, max_value=100))
86+
@settings(deadline=None, max_examples=50)
87+
def test_mix_properties_repetitions_good_values(good_reps: int) -> None:
88+
"""Test valid int >= 0 for MixProperties repetitions."""
89+
_build_mix_properties(
90+
MixProperties(enable=True, params=MixParams(repetitions=good_reps, volume=5))
91+
)
92+
mp = _build_mix_properties(
93+
MixProperties(enable=True, params=MixParams(repetitions=2, volume=5))
94+
)
95+
mp.repetitions = good_reps
96+
assert mp.repetitions == good_reps
97+
98+
99+
@given(bad_reps=st.one_of(st.integers(max_value=-1), invalid_values))
100+
@settings(deadline=None, max_examples=50)
101+
def test_mix_properties_repetitions_bad_values(bad_reps: Any) -> None:
102+
"""Test invalid repetitions < 1 or non-integer."""
103+
with pytest.raises(ValidationError):
104+
_build_mix_properties(
105+
MixProperties(enable=True, params=MixParams(repetitions=bad_reps, volume=5))
106+
)
107+
mp = _build_mix_properties(
108+
MixProperties(enable=True, params=MixParams(repetitions=3, volume=5))
109+
)
110+
with pytest.raises(ValueError):
111+
mp.repetitions = bad_reps

shared-data/command/schemas/12.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -3068,17 +3068,19 @@
30683068
"description": "Parameters for mix.",
30693069
"properties": {
30703070
"repetitions": {
3071-
"description": "Number of mixing repetitions.",
3071+
"description": "Number of mixing repetitions. 0 is valid, but no mixing will occur.",
30723072
"minimum": 0,
30733073
"title": "Repetitions",
30743074
"type": "integer"
30753075
},
30763076
"volume": {
30773077
"anyOf": [
30783078
{
3079+
"exclusiveMinimum": 0,
30793080
"type": "integer"
30803081
},
30813082
{
3083+
"exclusiveMinimum": 0.0,
30823084
"type": "number"
30833085
}
30843086
],

shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,18 @@ class MixParams(BaseModel):
139139
"""Parameters for mix."""
140140

141141
repetitions: _StrictNonNegativeInt = Field(
142-
..., description="Number of mixing repetitions."
142+
...,
143+
description="Number of mixing repetitions. 0 is valid, but no mixing will occur.",
144+
)
145+
volume: _GreaterThanZeroNumber = Field(
146+
..., description="Volume used for mixing, in microliters."
143147
)
144-
volume: _Number = Field(..., description="Volume used for mixing, in microliters.")
145148

146149

147150
class MixProperties(BaseModel):
148151
"""Mixing properties."""
149152

150-
enable: bool = Field(..., description="Whether mix is enabled.")
153+
enable: StrictBool = Field(..., description="Whether mix is enabled.")
151154
params: MixParams | SkipJsonSchema[None] = Field(
152155
None,
153156
description="Parameters for the mix function.",

0 commit comments

Comments
 (0)