diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 53e9a6393082..e65766f30051 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -392,12 +392,7 @@ def dispense( ) ) - if isinstance(location, (TrashBin, WasteChute)): - self._protocol_core.set_last_location(location=None, mount=self.get_mount()) - else: - self._protocol_core.set_last_location( - location=location, mount=self.get_mount() - ) + self._protocol_core.set_last_location(location=location, mount=self.get_mount()) def blow_out( self, @@ -473,12 +468,7 @@ def blow_out( ) ) - if isinstance(location, (TrashBin, WasteChute)): - self._protocol_core.set_last_location(location=None, mount=self.get_mount()) - else: - self._protocol_core.set_last_location( - location=location, mount=self.get_mount() - ) + self._protocol_core.set_last_location(location=location, mount=self.get_mount()) def touch_tip( self, @@ -806,12 +796,8 @@ def move_to( speed=speed, ) ) - if isinstance(location, (TrashBin, WasteChute)): - self._protocol_core.set_last_location(location=None, mount=self.get_mount()) - else: - self._protocol_core.set_last_location( - location=location, mount=self.get_mount() - ) + + self._protocol_core.set_last_location(location=location, mount=self.get_mount()) def resin_tip_seal( self, location: Location, well_core: WellCore, in_place: Optional[bool] = False diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index b62e3371ced1..e7fb8d23bbf4 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -112,7 +112,7 @@ def __init__( self._engine_client = engine_client self._api_version = api_version self._sync_hardware = sync_hardware - self._last_location: Optional[Location] = None + self._last_location: Optional[Union[Location, TrashBin, WasteChute]] = None self._last_mount: Optional[Mount] = None self._labware_cores_by_id: Dict[str, LabwareCore] = {} self._module_cores_by_id: Dict[ @@ -892,7 +892,7 @@ def door_closed(self) -> bool: def get_last_location( self, mount: Optional[Mount] = None, - ) -> Optional[Location]: + ) -> Optional[Union[Location, TrashBin, WasteChute]]: """Get the last accessed location.""" if mount is None or mount == self._last_mount: return self._last_location @@ -901,7 +901,7 @@ def get_last_location( def set_last_location( self, - location: Optional[Location], + location: Optional[Union[Location, TrashBin, WasteChute]], mount: Optional[Mount] = None, ) -> None: """Set the last accessed location.""" diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index 3a35fdd824e1..b660fd099ce3 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -216,7 +216,7 @@ def door_closed(self) -> bool: def get_last_location( self, mount: Optional[Mount] = None, - ) -> Optional[Location]: + ) -> Optional[Union[Location, TrashBin, WasteChute]]: ... @abstractmethod diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index ad9245f9d76c..10197c1123b1 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -258,7 +258,7 @@ def aspirate( "knows where it is." ) from e - if isinstance(target, (TrashBin, WasteChute)): + if isinstance(target, validation.DisposalTarget): raise ValueError( "Trash Bin and Waste Chute are not acceptable location parameters for Aspirate commands." ) @@ -399,6 +399,10 @@ def dispense( .. versionchanged:: 2.17 Behavior of the ``volume`` parameter. + + .. versionchanged:: 2.24 + ``location`` is no longer required if the pipette just moved to, dispensed, or blew out + into a trash bin or waste chute. """ if self.api_version < APIVersion(2, 15) and push_out: raise APIVersionError( @@ -432,13 +436,13 @@ def dispense( flow_rate = self._core.get_dispense_flow_rate(rate) - if isinstance(target, (TrashBin, WasteChute)): + if isinstance(target, validation.DisposalTarget): with publisher.publish_context( broker=self.broker, command=cmds.dispense_in_disposal_location( instrument=self, volume=c_vol, - location=target, + location=target.location, rate=rate, flow_rate=flow_rate, ), @@ -446,10 +450,10 @@ def dispense( self._core.dispense( volume=c_vol, rate=rate, - location=target, + location=target.location, well_core=None, flow_rate=flow_rate, - in_place=False, + in_place=target.in_place, push_out=push_out, meniscus_tracking=None, ) @@ -656,6 +660,10 @@ def blow_out( without first calling a method that takes a location, like :py:meth:`.aspirate` or :py:meth:`dispense`. :returns: This instance. + + .. versionchanged:: 2.24 + ``location`` is no longer required if the pipette just moved to, dispensed, or blew out + into a trash bin or waste chute. """ well: Optional[labware.Well] = None move_to_location: types.Location @@ -696,17 +704,17 @@ def blow_out( well = target.well elif isinstance(target, validation.PointTarget): move_to_location = target.location - elif isinstance(target, (TrashBin, WasteChute)): + elif isinstance(target, validation.DisposalTarget): with publisher.publish_context( broker=self.broker, command=cmds.blow_out_in_disposal_location( - instrument=self, location=target + instrument=self, location=target.location ), ): self._core.blow_out( - location=target, + location=target.location, well_core=None, - in_place=False, + in_place=target.in_place, ) return self @@ -793,8 +801,12 @@ def touch_tip( # noqa: C901 # If location is a valid well, move to the well first if location is None: last_location = self._protocol_core.get_last_location() - if not last_location: - raise RuntimeError("No valid current location cache present") + if last_location is None or isinstance( + last_location, (TrashBin, WasteChute) + ): + raise RuntimeError( + f"Cached location of {last_location} is not valid for touch tip." + ) parent_labware, well = last_location.labware.get_parent_labware_and_well() if not well or not parent_labware: raise RuntimeError( @@ -896,10 +908,10 @@ def air_gap( # noqa: C901 .. versionchanged:: 2.22 No longer implemented as an aspirate. - .. versionchanged:: 2.24 Adds the ``rate`` and ``flow_rate`` parameter. You can only define one or the other. If both are unspecified then ``rate`` is by default set to 1.0. + Can air gap over a trash bin or waste chute. """ if not self._core.has_tip(): raise UnexpectedTipRemovalError("air_gap", self.name, self.mount) @@ -923,10 +935,23 @@ def air_gap( # noqa: C901 if height is None: height = 5 - loc = self._protocol_core.get_last_location() - if not loc or not loc.labware.is_well: - raise RuntimeError("No previous Well cached to perform air gap") - target = loc.labware.as_well().top(height) + last_location = self._protocol_core.get_last_location() + if self.api_version < APIVersion(2, 24) and isinstance( + last_location, (TrashBin, WasteChute) + ): + last_location = None + if last_location is None or ( + isinstance(last_location, types.Location) + and not last_location.labware.is_well + ): + raise RuntimeError( + f"Cached location of {last_location} is not valid for air gap." + ) + target: Union[types.Location, TrashBin, WasteChute] + if isinstance(last_location, types.Location): + target = last_location.labware.as_well().top(height) + else: + target = last_location.top(height) self.move_to(target, publish=False) if self.api_version >= _AIR_GAP_TRACKING_ADDED_IN: self._core.prepare_to_aspirate() @@ -2583,14 +2608,22 @@ def well_bottom_clearance(self) -> "Clearances": """ return self._well_bottom_clearances - def _get_last_location_by_api_version(self) -> Optional[types.Location]: + def _get_last_location_by_api_version( + self, + ) -> Optional[Union[types.Location, TrashBin, WasteChute]]: """Get the last location accessed by this pipette, if any. In pre-engine Protocol API versions, this call omits the pipette mount. + Between 2.14 (first engine PAPI version) and 2.23 this only returns None or a Location object. This is to preserve pre-existing, potentially buggy behavior. """ - if self._api_version >= ENGINE_CORE_API_VERSION: + if self._api_version >= APIVersion(2, 24): return self._protocol_core.get_last_location(mount=self._core.get_mount()) + elif self._api_version >= ENGINE_CORE_API_VERSION: + last_location = self._protocol_core.get_last_location( + mount=self._core.get_mount() + ) + return last_location if isinstance(last_location, types.Location) else None else: return self._protocol_core.get_last_location() @@ -2646,7 +2679,11 @@ def configure_for_volume(self, volume: float) -> None: actual_value=str(volume), ) last_location = self._get_last_location_by_api_version() - if last_location and isinstance(last_location.labware, labware.Well): + if ( + last_location + and isinstance(last_location, types.Location) + and isinstance(last_location.labware, labware.Well) + ): self.move_to(last_location.labware.top()) self._core.configure_for_volume(volume) diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 253215e3219f..59f46689808e 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -1190,9 +1190,16 @@ def home(self) -> None: self._core.home() @property - def location_cache(self) -> Optional[Location]: - """The cache used by the robot to determine where it last was.""" - return self._core.get_last_location() + def location_cache(self) -> Optional[Union[Location, TrashBin, WasteChute]]: + """The cache used by the robot to determine where it last was. + + .. versionchanged:: 2.24 + Can return a ``TrashBin`` or ``WasteChute`` object. + """ + last_loc = self._core.get_last_location() + if isinstance(last_loc, Location) or self._api_version >= APIVersion(2, 24): + return last_loc + return None @location_cache.setter def location_cache(self, loc: Optional[Location]) -> None: diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 178a6fc54d8a..2d323a1fce4d 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -545,6 +545,11 @@ class PointTarget(NamedTuple): in_place: bool +class DisposalTarget(NamedTuple): + location: Union[TrashBin, WasteChute] + in_place: bool + + class NoLocationError(ValueError): """Error representing that no location was supplied.""" @@ -553,12 +558,12 @@ class LocationTypeError(TypeError): """Error representing that the location supplied is of different expected type.""" -ValidTarget = Union[WellTarget, PointTarget, TrashBin, WasteChute] +ValidTarget = Union[WellTarget, PointTarget, DisposalTarget] def validate_location( - location: Union[Location, Well, TrashBin, WasteChute, None], - last_location: Optional[Location], + location: Optional[Union[Location, Well, TrashBin, WasteChute]], + last_location: Optional[Union[Location, TrashBin, WasteChute]], ) -> ValidTarget: """Validate a given location for a liquid handling command. @@ -569,9 +574,11 @@ def validate_location( Returns: A `WellTarget` if the input location represents a well. A `PointTarget` if the input location is an x, y, z coordinate. + A `TrashBin` if the input location is a trash bin + A `WasteChute` if the input location is a waste chute Raises: - NoLocationError: The is no input location and no cached loaction. + NoLocationError: There is no input location and no cached location. LocationTypeError: The location supplied is of unexpected type. """ from .labware import Well @@ -586,11 +593,11 @@ def validate_location( f"location should be a Well, Location, TrashBin or WasteChute, but it is {location}" ) - if isinstance(target_location, (TrashBin, WasteChute)): - return target_location - in_place = target_location == last_location + if isinstance(target_location, (TrashBin, WasteChute)): + return DisposalTarget(location=target_location, in_place=in_place) + if isinstance(target_location, Well): return WellTarget(well=target_location, location=None, in_place=in_place) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index ebe4171caa4f..98d0ed1e9c5c 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -409,6 +409,43 @@ def test_move_to_coordinates( ) +def test_move_to_trash( + decoy: Decoy, + mock_engine_client: EngineClient, + mock_protocol_core: ProtocolCore, + subject: InstrumentCore, +) -> None: + """It should move the pipette to a trash and update the location cache.""" + mock_trash = decoy.mock(cls=TrashBin) + + decoy.when(mock_trash.offset).then_return(DisposalOffset(x=1, y=2, z=3)) + decoy.when(mock_trash.area_name).then_return("waste management") + + subject.move_to( + location=mock_trash, + well_core=None, + force_direct=True, + minimum_z_height=42.0, + speed=4.56, + ) + + decoy.verify( + mock_engine_client.execute_command( + cmd.MoveToAddressableAreaForDropTipParams( + pipetteId="abc123", + addressableAreaName="waste management", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=True, + speed=4.56, + minimumZHeight=None, + alternateDropLocation=False, + ignoreTipConfiguration=True, + ) + ), + mock_protocol_core.set_last_location(location=mock_trash, mount=Mount.LEFT), + ) + + def test_pick_up_tip( decoy: Decoy, mock_engine_client: EngineClient, @@ -974,6 +1011,48 @@ def test_blow_out_in_place( ) +def test_blow_out_to_trash_bin( + decoy: Decoy, + mock_engine_client: EngineClient, + mock_protocol_core: ProtocolCore, + subject: InstrumentCore, +) -> None: + """It should move to a trash and blow out there.""" + decoy.when(mock_protocol_core.api_version).then_return(MAX_SUPPORTED_VERSION) + mock_trash = decoy.mock(cls=TrashBin) + + decoy.when(mock_trash.offset).then_return(DisposalOffset(x=1, y=2, z=3)) + decoy.when(mock_trash.area_name).then_return("rubbish") + + subject.blow_out( + location=mock_trash, + well_core=None, + in_place=False, + ) + + decoy.verify( + mock_engine_client.execute_command( + cmd.MoveToAddressableAreaForDropTipParams( + pipetteId="abc123", + addressableAreaName="rubbish", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=False, + speed=None, + minimumZHeight=None, + alternateDropLocation=False, + ignoreTipConfiguration=True, + ) + ), + mock_engine_client.execute_command( + cmd.BlowOutInPlaceParams( + pipetteId="abc123", + flowRate=6.7, + ) + ), + mock_protocol_core.set_last_location(location=mock_trash, mount=Mount.LEFT), + ) + + def test_dispense_to_well( decoy: Decoy, mock_engine_client: EngineClient, @@ -1121,6 +1200,56 @@ def test_dispense_to_coordinates( ) +def test_dispense_to_trash_bin( + decoy: Decoy, + mock_engine_client: EngineClient, + mock_protocol_core: ProtocolCore, + subject: InstrumentCore, +) -> None: + """It should move to a trash and dispense there.""" + decoy.when(mock_protocol_core.api_version).then_return(MAX_SUPPORTED_VERSION) + mock_trash = decoy.mock(cls=TrashBin) + + decoy.when(mock_trash.offset).then_return(DisposalOffset(x=1, y=2, z=3)) + decoy.when(mock_trash.area_name).then_return("garbage day") + + subject.dispense( + volume=12.34, + rate=5.6, + flow_rate=7.8, + well_core=None, + location=mock_trash, + in_place=False, + push_out=None, + meniscus_tracking=None, + ) + + decoy.verify( + mock_engine_client.execute_command( + cmd.MoveToAddressableAreaForDropTipParams( + pipetteId="abc123", + addressableAreaName="garbage day", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=False, + speed=None, + minimumZHeight=None, + alternateDropLocation=False, + ignoreTipConfiguration=True, + ) + ), + mock_engine_client.execute_command( + cmd.DispenseInPlaceParams( + pipetteId="abc123", + volume=12.34, + correctionVolume=None, + flowRate=7.8, + pushOut=None, + ) + ), + mock_protocol_core.set_last_location(location=mock_trash, mount=Mount.LEFT), + ) + + @pytest.mark.parametrize( ("api_version", "expect_clampage"), [(APIVersion(2, 16), True), (APIVersion(2, 17), False)], diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index bb9966de4f62..4103e64fb997 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -586,6 +586,53 @@ def test_blow_out_to_location( ) +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 24))) +def test_blow_out_with_trash_last_location( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should blow out into a previously accessed disposal location.""" + mock_chute = decoy.mock(cls=WasteChute) + decoy.when(mock_instrument_core.get_mount()).then_return(Mount.LEFT) + decoy.when(mock_protocol_core.get_last_location(mount=Mount.LEFT)).then_return( + mock_chute + ) + subject.blow_out() + + decoy.verify( + mock_instrument_core.blow_out( + location=mock_chute, well_core=None, in_place=True + ), + times=1, + ) + + +@pytest.mark.parametrize( + "api_version", + versions_between( + low_exclusive_bound=APIVersion(2, 13), high_inclusive_bound=APIVersion(2, 23) + ), +) +def test_blow_out_with_trash_last_location_raises_earlier_api( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should raise if a trash is the last accessed location and on 2.23.""" + mock_trash = decoy.mock(cls=TrashBin) + decoy.when(mock_instrument_core.get_mount()).then_return(Mount.LEFT) + decoy.when(mock_protocol_core.get_last_location(mount=Mount.LEFT)).then_return( + mock_trash + ) + with pytest.raises( + RuntimeError, match="blow out is called without an explicit location" + ): + subject.blow_out() + + def test_blow_out_raises_no_location( decoy: Decoy, mock_instrument_core: InstrumentCore, @@ -1132,6 +1179,25 @@ def test_touch_tip( ) +def test_touch_tip_raises_if_trash_last_location( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should raise if the last location was a trash bin or waste chute.""" + decoy.when(mock_instrument_core.has_tip()).then_return(True) + mock_trash = decoy.mock(cls=TrashBin) + decoy.when(mock_protocol_core.get_last_location()).then_return(mock_trash) + with pytest.raises(RuntimeError, match="not valid for touch tip"): + subject.touch_tip() + + mock_chute = decoy.mock(cls=WasteChute) + decoy.when(mock_protocol_core.get_last_location()).then_return(mock_chute) + with pytest.raises(RuntimeError, match="not valid for touch tip"): + subject.touch_tip() + + def test_return_height( decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext ) -> None: @@ -1417,6 +1483,61 @@ def test_aspirate_0_volume_means_aspirate_nothing( ) +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 24))) +def test_dispense_with_trash_last_location( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should dispense into a previously accessed trash.""" + mock_trash = decoy.mock(cls=TrashBin) + decoy.when(mock_instrument_core.get_mount()).then_return(Mount.LEFT) + decoy.when(mock_protocol_core.get_last_location(mount=Mount.LEFT)).then_return( + mock_trash + ) + decoy.when(mock_instrument_core.get_dispense_flow_rate(4.5)).then_return(6.7) + subject.dispense(volume=12.3, rate=4.5) + + decoy.verify( + mock_instrument_core.dispense( + location=mock_trash, + well_core=None, + in_place=True, + volume=12.3, + rate=4.5, + flow_rate=6.7, + push_out=None, + meniscus_tracking=None, + ), + times=1, + ) + + +@pytest.mark.parametrize( + "api_version", + versions_between( + low_exclusive_bound=APIVersion(2, 13), high_inclusive_bound=APIVersion(2, 23) + ), +) +def test_dispense_with_trash_last_location_raises_earlier_api( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should raise if a trash is the last accessed location and on 2.23.""" + mock_trash = decoy.mock(cls=TrashBin) + decoy.when(mock_instrument_core.get_mount()).then_return(Mount.LEFT) + decoy.when(mock_protocol_core.get_last_location(mount=Mount.LEFT)).then_return( + mock_trash + ) + with pytest.raises( + RuntimeError, match="dispense is called without an explicit location" + ): + subject.dispense(volume=12.3, rate=4.5) + + @pytest.mark.parametrize("api_version", [APIVersion(2, 20)]) def test_detect_liquid_presence( decoy: Decoy, @@ -1923,6 +2044,60 @@ def test_air_gap_has_flow_rate_and_rate( subject.air_gap(volume=10, height=5, flow_rate=100, rate=5.0) +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 24))) +def test_air_gap_over_trash( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """It should air gap over a disposal location.""" + mock_trash = decoy.mock(cls=TrashBin) + mock_trash_2 = decoy.mock(cls=TrashBin) + mock_move_to = decoy.mock(func=subject.move_to) + monkeypatch.setattr(subject, "move_to", mock_move_to) + + decoy.when(mock_instrument_core.has_tip()).then_return(True) + decoy.when(mock_protocol_core.get_last_location()).then_return(mock_trash) + decoy.when(mock_instrument_core.get_aspirate_flow_rate()).then_return(11) + decoy.when(mock_trash.top(11)).then_return(mock_trash_2) + + subject.air_gap(volume=10, height=11) + + decoy.verify(mock_move_to(mock_trash_2, publish=False)) + decoy.verify(mock_instrument_core.prepare_to_aspirate()) + decoy.verify(mock_instrument_core.air_gap_in_place(10, 11)) + + +@pytest.mark.parametrize( + "api_version", + versions_between( + low_exclusive_bound=APIVersion(2, 13), high_inclusive_bound=APIVersion(2, 23) + ), +) +def test_air_gap_over_trash_or_waste_chute_raises( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, +) -> None: + """It should raise if a disposal location is the last accessed on versions below 2.23.""" + mock_chute = decoy.mock(cls=WasteChute) + mock_trash = decoy.mock(cls=TrashBin) + + decoy.when(mock_instrument_core.has_tip()).then_return(True) + decoy.when(mock_protocol_core.get_last_location()).then_return(mock_chute) + + with pytest.raises(RuntimeError, match="not valid for air gap"): + subject.air_gap(volume=10, height=11) + + decoy.when(mock_protocol_core.get_last_location()).then_return(mock_trash) + + with pytest.raises(RuntimeError, match="not valid for air gap"): + subject.air_gap(volume=10, height=11) + + @pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) def test_transfer_liquid_raises_for_invalid_locations( decoy: Decoy, diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 6522762d6d1b..29be970966e5 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -581,6 +581,40 @@ def test_validate_last_location_with_labware(decoy: Decoy) -> None: assert result == subject.PointTarget(location=input_last_location, in_place=True) +def test_validate_location_with_trash_bin(decoy: Decoy) -> None: + """Should return a Trash Bin object.""" + mock_trash = decoy.mock(cls=TrashBin) + + result = subject.validate_location(location=None, last_location=mock_trash) + assert result.location is mock_trash + assert result.in_place + + result = subject.validate_location(location=mock_trash, last_location=None) + assert result.location is mock_trash + assert not result.in_place + + result = subject.validate_location(location=mock_trash, last_location=mock_trash) + assert result.location is mock_trash + assert result.in_place + + +def test_validate_location_with_waste_chute(decoy: Decoy) -> None: + """Should return a waste chute object.""" + mock_chute = decoy.mock(cls=WasteChute) + + result = subject.validate_location(location=None, last_location=mock_chute) + assert result.location is mock_chute + assert result.in_place + + result = subject.validate_location(location=mock_chute, last_location=None) + assert result.location is mock_chute + assert not result.in_place + + result = subject.validate_location(location=mock_chute, last_location=mock_chute) + assert result.location is mock_chute + assert result.in_place + + def test_ensure_boolean() -> None: """It should return a boolean value.""" assert subject.ensure_boolean(False) is False