Skip to content

Commit

Permalink
WIP PE get_errors_slice. added tests to robot server
Browse files Browse the repository at this point in the history
  • Loading branch information
TamarZanzouri committed Aug 2, 2024
1 parent 6e94b90 commit 0ed2a97
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 14 deletions.
10 changes: 9 additions & 1 deletion api/src/opentrons/protocol_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@
CommandType,
CommandIntent,
)
from .state import State, StateView, StateSummary, CommandSlice, CommandPointer, Config, CommandErrorSlice
from .state import (
State,
StateView,
StateSummary,
CommandSlice,
CommandPointer,
Config,
CommandErrorSlice,
)
from .plugins import AbstractPlugin

from .types import (
Expand Down
34 changes: 34 additions & 0 deletions api/src/opentrons/protocol_engine/state/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class CommandSlice:
cursor: int
total_length: int


@dataclass(frozen=True)
class CommandErrorSlice:
"""A subset of all commands errors in state."""
Expand Down Expand Up @@ -627,6 +628,39 @@ def get_slice(
total_length=total_length,
)

def get_errors_slice(
self,
cursor: Optional[int],
length: int,
) -> CommandErrorSlice:
"""Get a subset of commands error around a given cursor.
If the cursor is omitted, a cursor will be selected automatically
based most recent error.
"""
all_errors = self.get_all_errors()
total_length = len(all_errors)

if cursor is None:
if len(all_errors) > 0:
# Get the most recent error,
# which we can find just at the end of the list.
cursor = total_length - 1
else:
cursor = total_length - length

# start is inclusive, stop is exclusive
actual_cursor = max(0, min(cursor, total_length - 1))
stop = min(total_length, actual_cursor + length)

sliced_errors = all_errors[actual_cursor:stop]

return CommandErrorSlice(
commands_errors=sliced_errors,
cursor=actual_cursor,
total_length=total_length,
)

def get_error(self) -> Optional[ErrorOccurrence]:
"""Get the run's fatal error, if there was one."""
run_error = self._state.run_error
Expand Down
16 changes: 16 additions & 0 deletions api/src/opentrons/protocol_runner/run_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
StateSummary,
CommandPointer,
CommandSlice,
CommandErrorSlice,
DeckType,
)
from ..protocol_engine.errors import RunStoppedError
Expand Down Expand Up @@ -269,6 +270,21 @@ def get_command_slice(
cursor=cursor, length=length
)

def get_command_error_slice(
self,
cursor: Optional[int],
length: int,
) -> CommandErrorSlice:
"""Get a slice of run commands.
Args:
cursor: Requested index of first command in the returned slice.
length: Length of slice to return.
"""
return self._protocol_engine.state_view.commands.get_slice(
cursor=cursor, length=length
)

def get_command_recovery_target(self) -> Optional[CommandPointer]:
"""Get the current error recovery target."""
return self._protocol_engine.state_view.commands.get_recovery_target()
Expand Down
4 changes: 3 additions & 1 deletion robot-server/robot_server/runs/router/commands_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,9 @@ async def get_run_commands_error(
__root__=command_error_slice.commands_errors
)
return await PydanticResponse.create(
content=SimpleMultiBody.construct(data=commands_errors_result, meta=meta),
content=SimpleMultiBody.construct(
data=commands_errors_result, meta=meta # type: ignore[arg-type]
),
status_code=status.HTTP_200_OK,
)

Expand Down
47 changes: 36 additions & 11 deletions robot-server/tests/runs/router/test_commands_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
)

from robot_server.errors.error_responses import ApiError
from robot_server.service.json_api import MultiBodyMeta, SimpleMultiBody
from robot_server.service.json_api import MultiBodyMeta, SimpleMultiBody, ResponseList

from robot_server.runs.command_models import (
RequestModelWithCommandCreate,
Expand All @@ -24,7 +24,7 @@
)
from robot_server.runs.run_store import CommandNotFoundError, RunStore
from robot_server.runs.run_orchestrator_store import RunOrchestratorStore
from robot_server.runs.run_data_manager import RunDataManager
from robot_server.runs.run_data_manager import RunDataManager, RunNotCurrentError
from robot_server.runs.run_models import RunCommandSummary, RunNotFoundError
from robot_server.runs.router.commands_router import (
create_run_command,
Expand Down Expand Up @@ -448,6 +448,29 @@ async def test_get_run_commands_errors(
) -> None:
"""It should return a list of all commands errors in a run."""

decoy.when(
mock_run_data_manager.get_command_error_slice(
run_id="run-id",
cursor=None,
length=42,
)
).then_raise(RunNotCurrentError("oh no!"))

with pytest.raises(ApiError):
result = await get_run_commands_error(
runId="run-id",
run_data_manager=mock_run_data_manager,
cursor=None,
pageLength=42,
)
assert result.status_code == 409


async def test_get_run_commands_errors_raises_no_run(
decoy: Decoy, mock_run_data_manager: RunDataManager
) -> None:
"""It should return a list of all commands errors in a run."""

error = (
pe_errors.ErrorOccurrence(
id="error-id",
Expand All @@ -458,7 +481,7 @@ async def test_get_run_commands_errors(
)

command_error_slice = CommandErrorSlice(
cursor=1, total_length=3, commands_errors=[error]
cursor=1, total_length=3, commands_errors=list(error)
)

decoy.when(
Expand All @@ -476,14 +499,16 @@ async def test_get_run_commands_errors(
pageLength=42,
)

assert result.content.data == [
pe_errors.ErrorOccurrence(
id="error-id",
errorType="PrettyBadError",
createdAt=datetime(year=2024, month=4, day=4),
detail="Things are not looking good.",
)
]
assert result.content.data == ResponseList(
__root__=[
pe_errors.ErrorOccurrence(
id="error-id",
errorType="PrettyBadError",
createdAt=datetime(year=2024, month=4, day=4),
detail="Things are not looking good.",
)
]
)
assert result.content.meta == MultiBodyMeta(cursor=1, totalLength=3)
assert result.status_code == 200

Expand Down
2 changes: 1 addition & 1 deletion robot-server/tests/runs/test_run_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ def test_get_commands_errors_slice__not_current_run_raises(
decoy.when(mock_run_orchestrator_store.current_run_id).then_return("run-not-id")

with pytest.raises(RunNotCurrentError):
result = subject.get_commands_slice("run-id", 1, 2)
result = subject.get_command_error_slice("run-id", 1, 2)


def test_get_commands_errors_slice_current_run(
Expand Down

0 comments on commit 0ed2a97

Please sign in to comment.