diff --git a/pylabrobot/resources/hamilton/hamilton_deck_tests.py b/pylabrobot/resources/hamilton/hamilton_deck_tests.py index d41199de305..ca84a90cb91 100644 --- a/pylabrobot/resources/hamilton/hamilton_deck_tests.py +++ b/pylabrobot/resources/hamilton/hamilton_deck_tests.py @@ -1,7 +1,7 @@ import textwrap import unittest -from pylabrobot.resources import TipRack +from pylabrobot.resources import Deck, TipRack from pylabrobot.resources.corning import ( cor_96_wellplate_360uL_Fb, ) @@ -94,6 +94,30 @@ def test_teaching_rack_excluded_by_nominal_volume_search(self): self.assertEqual(len(matches), 5) self.assertNotIn("teaching_tip_rack", {tr.name for tr in matches}) + def test_get_trash_area96_survives_serialization_round_trip(self): + """`serialize()` encodes the 96 trash as a child and sets `with_trash96=False`, so the + rebuilt deck's `__init__` leaves the cached `_trash96` pointer as None. `get_trash_area96()` + must still resolve it from the child tree rather than raising.""" + deck = self.build_layout() + original_location = deck.get_trash_area96().get_location_wrt(deck) + + deck2 = Deck.deserialize(deck.serialize()) + + self.assertIn("trash_core96", [child.name for child in deck2.children]) + self.assertEqual(deck2.get_trash_area96().get_location_wrt(deck2), original_location) + + def test_get_trash_area96_raises_after_clear_include_trash(self): + """`clear(include_trash=True)` unassigns the 96 trash. `get_trash_area96()` must raise + rather than hand back the now-orphaned resource via the stale cached pointer.""" + deck = self.build_layout() + deck.get_trash_area96() # populate the cache before invalidating it + + deck.clear(include_trash=True) + + self.assertNotIn("trash_core96", [child.name for child in deck.children]) + with self.assertRaises(RuntimeError): + deck.get_trash_area96() + def test_assign_gigantic_resource(self): stanley_cup = StanleyCup_QUENCHER_FLOWSTATE_TUMBLER(name="HUGE") deck = STARLetDeck() diff --git a/pylabrobot/resources/hamilton/hamilton_decks.py b/pylabrobot/resources/hamilton/hamilton_decks.py index 8c0b9d3fff9..c36d0aa9128 100644 --- a/pylabrobot/resources/hamilton/hamilton_decks.py +++ b/pylabrobot/resources/hamilton/hamilton_decks.py @@ -548,11 +548,20 @@ def rails_to_location(self, rails: int) -> Coordinate: return Coordinate(x=x, y=63, z=100) def get_trash_area96(self) -> Trash: - if self._trash96 is None: - raise RuntimeError( - "Trash area for 96-well plates was not created. Initialize with `with_trash96=True`." - ) - return self._trash96 + # `_trash96` is only set in `__init__`, so it is stale whenever the deck was rebuilt from a + # serialized layout (which encodes the trash as a child and passes `with_trash96=False`) or the + # trash was unassigned. Resolve against the child tree instead, and only trust the cached + # reference while it is still assigned to this deck. + if self._trash96 is not None and self._trash96.parent is self: + return self._trash96 + + if self.has_resource("trash_core96"): + self._trash96 = cast(Trash, self.get_resource("trash_core96")) + return self._trash96 + + raise RuntimeError( + "Trash area for 96-well plates was not created. Initialize with `with_trash96=True`." + ) def clear(self, include_trash: bool = False): """Clear the deck, removing all resources except the trash areas and the waste block."""