diff --git a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py index b709c34367b..24c0f14a3b4 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -45,7 +45,7 @@ ) from opentrons_shared_data.errors.exceptions import EStopActivatedError, PythonException -from opentrons.protocol_engine.notes import CommandNoteTracker +from opentrons.protocol_engine.notes import CommandNoteTracker, CommandNote @pytest.fixture @@ -445,3 +445,244 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: ) ), ) + + +async def test_executor_forwards_notes_on_command_success( + decoy: Decoy, + hardware_api: HardwareControlAPI, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + equipment: EquipmentHandler, + movement: MovementHandler, + mock_gantry_mover: GantryMover, + labware_movement: LabwareMovementHandler, + pipetting: PipettingHandler, + mock_tip_handler: TipHandler, + run_control: RunControlHandler, + rail_lights: RailLightsHandler, + status_bar: StatusBarHandler, + model_utils: ModelUtils, + command_note_tracker: CommandNoteTracker, + subject: CommandExecutor, +) -> None: + """It should be able to add notes during OK execution to command updates.""" + TestCommandImplCls = decoy.mock(func=_TestCommandImpl) + command_impl = decoy.mock(cls=_TestCommandImpl) + + class _TestCommand(BaseCommand[_TestCommandParams, _TestCommandResult]): + commandType: str = "testCommand" + params: _TestCommandParams + result: Optional[_TestCommandResult] + + @property + def _ImplementationCls(self) -> Type[_TestCommandImpl]: + return TestCommandImplCls + + command_params = _TestCommandParams() + command_result = _TestCommandResult() + + queued_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + status=CommandStatus.QUEUED, + params=command_params, + ), + ) + + command_notes = [ + CommandNote( + noteKind="warning", + shortMessage="hello", + longMessage="test command note", + source="test", + ) + ] + + running_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + startedAt=datetime(year=2022, month=2, day=2), + status=CommandStatus.RUNNING, + params=command_params, + ), + ) + + completed_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + startedAt=datetime(year=2022, month=2, day=2), + completedAt=datetime(year=2023, month=3, day=3), + status=CommandStatus.SUCCEEDED, + params=command_params, + result=command_result, + notes=command_notes, + ), + ) + + decoy.when(state_store.commands.get(command_id="command-id")).then_return( + queued_command + ) + + decoy.when( + queued_command._ImplementationCls( + state_view=state_store, + hardware_api=hardware_api, + equipment=equipment, + movement=movement, + gantry_mover=mock_gantry_mover, + labware_movement=labware_movement, + pipetting=pipetting, + tip_handler=mock_tip_handler, + run_control=run_control, + rail_lights=rail_lights, + status_bar=status_bar, + command_note_adder=command_note_tracker, + ) + ).then_return( + command_impl # type: ignore[arg-type] + ) + + decoy.when(await command_impl.execute(command_params)).then_return(command_result) + + decoy.when(model_utils.get_timestamp()).then_return( + datetime(year=2022, month=2, day=2), + datetime(year=2023, month=3, day=3), + ) + decoy.when(command_note_tracker.get_notes()).then_return(command_notes) + + await subject.execute("command-id") + + decoy.verify( + action_dispatcher.dispatch( + UpdateCommandAction(private_result=None, command=running_command) + ), + action_dispatcher.dispatch( + UpdateCommandAction(private_result=None, command=completed_command) + ), + ) + +async def test_executor_forwards_notes_on_command_failure( + decoy: Decoy, + hardware_api: HardwareControlAPI, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + equipment: EquipmentHandler, + movement: MovementHandler, + mock_gantry_mover: GantryMover, + labware_movement: LabwareMovementHandler, + pipetting: PipettingHandler, + mock_tip_handler: TipHandler, + run_control: RunControlHandler, + rail_lights: RailLightsHandler, + status_bar: StatusBarHandler, + model_utils: ModelUtils, + subject: CommandExecutor, + command_note_tracker: CommandNoteTracker, +) -> None: + """It should handle an error occuring during execution.""" + TestCommandImplCls = decoy.mock(func=_TestCommandImpl) + command_impl = decoy.mock(cls=_TestCommandImpl) + + + class _TestCommand(BaseCommand[_TestCommandParams, _TestCommandResult]): + commandType: str = "testCommand" + params: _TestCommandParams + result: Optional[_TestCommandResult] + + @property + def _ImplementationCls(self) -> Type[_TestCommandImpl]: + return TestCommandImplCls + + command_params = _TestCommandParams() + command_notes = [ + CommandNote( + noteKind="warning", + shortMessage="hello", + longMessage="test command note", + source="test", + ) + ] + + queued_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + status=CommandStatus.QUEUED, + params=command_params, + ), + ) + + running_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + startedAt=datetime(year=2022, month=2, day=2), + status=CommandStatus.RUNNING, + params=command_params, + ), + ) + running_command_with_notes = running_command.copy(update={'notes': command_notes}) + + decoy.when(state_store.commands.get(command_id="command-id")).then_return( + queued_command + ) + + decoy.when( + queued_command._ImplementationCls( + state_view=state_store, + hardware_api=hardware_api, + equipment=equipment, + movement=movement, + gantry_mover=mock_gantry_mover, + labware_movement=labware_movement, + pipetting=pipetting, + tip_handler=mock_tip_handler, + run_control=run_control, + rail_lights=rail_lights, + status_bar=status_bar, + command_note_adder=command_note_tracker, + ) + ).then_return( + command_impl # type: ignore[arg-type] + ) + + decoy.when(await command_impl.execute(command_params)).then_raise(RuntimeError("oh no")) + + decoy.when(model_utils.generate_id()).then_return("error-id") + decoy.when(model_utils.get_timestamp()).then_return( + datetime(year=2022, month=2, day=2), + datetime(year=2023, month=3, day=3), + ) + decoy.when(command_note_tracker.get_notes()).then_return(command_notes) + + await subject.execute("command-id") + + decoy.verify( + action_dispatcher.dispatch( + UpdateCommandAction(private_result=None, command=running_command) + ), + action_dispatcher.dispatch( + UpdateCommandAction(private_result=None, command=running_command_with_notes) + ), + action_dispatcher.dispatch( + FailCommandAction( + command_id="command-id", + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=matchers.ErrorMatching(PythonException, match="oh no"), + ) + ), + )