Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(robot-server): append error to analysis error list in case an internal error occurred #14510

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions robot-server/robot_server/errors/error_mappers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Map errors to Exceptions."""
from opentrons_shared_data.errors import EnumeratedError, PythonException


def map_unexpected_error(error: BaseException) -> EnumeratedError:
"""Map an unhandled Exception to a known exception."""
if isinstance(error, EnumeratedError):
return error
else:
return PythonException(error)
33 changes: 29 additions & 4 deletions robot-server/robot_server/protocols/protocol_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
import logging

from opentrons import protocol_runner
from opentrons.protocol_engine.errors import ErrorOccurrence
import opentrons.util.helpers as datetime_helper

import robot_server.errors.error_mappers as em

from .protocol_store import ProtocolResource
from .analysis_store import AnalysisStore


log = logging.getLogger(__name__)


Expand All @@ -30,9 +33,31 @@ async def analyze(
robot_type=protocol_resource.source.robot_type,
protocol_config=protocol_resource.source.config,
)
result = await runner.run(
protocol_source=protocol_resource.source, deck_configuration=[]
)
try:
result = await runner.run(
protocol_source=protocol_resource.source, deck_configuration=[]
)
except BaseException as error:
internal_error = em.map_unexpected_error(error=error)
await self._analysis_store.update(
analysis_id=analysis_id,
robot_type=protocol_resource.source.robot_type,
commands=[],
labware=[],
modules=[],
pipettes=[],
errors=[
ErrorOccurrence.from_failed(
# TODO(tz, 2-15-24): replace with a different error type
# when we are able to support different errors.
id="internal-error",
createdAt=datetime_helper.utc_now(),
error=internal_error,
)
],
liquids=[],
)
return

log.info(f'Completed analysis "{analysis_id}".')

Expand Down
99 changes: 99 additions & 0 deletions robot-server/tests/protocols/test_protocol_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,30 @@
)
import opentrons.protocol_runner as protocol_runner
from opentrons.protocol_reader import ProtocolSource, JsonProtocolConfig
import opentrons.util.helpers as datetime_helper

from robot_server.protocols.analysis_store import AnalysisStore
from robot_server.protocols.protocol_store import ProtocolResource
from robot_server.protocols.protocol_analyzer import ProtocolAnalyzer
import robot_server.errors.error_mappers as em

from opentrons_shared_data.errors import EnumeratedError, ErrorCodes


@pytest.fixture(autouse=True)
def patch_mock_map_unexpected_error(
decoy: Decoy, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Replace map_unexpected_error with a mock."""
mock_map_unexpected_error = decoy.mock(func=em.map_unexpected_error)
monkeypatch.setattr(em, "map_unexpected_error", mock_map_unexpected_error)


@pytest.fixture(autouse=True)
def patch_mock_get_utc_datetime(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None:
"""Replace utc_now with a mock."""
mock_get_utc_datetime = decoy.mock(func=datetime_helper.utc_now)
monkeypatch.setattr(datetime_helper, "utc_now", mock_get_utc_datetime)


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -146,3 +166,82 @@ async def test_analyze(
liquids=[],
),
)


async def test_analyze_updates_pending_on_error(
decoy: Decoy,
analysis_store: AnalysisStore,
subject: ProtocolAnalyzer,
) -> None:
"""It should update pending analysis with an internal error."""
robot_type: RobotType = "OT-3 Standard"

protocol_resource = ProtocolResource(
protocol_id="protocol-id",
created_at=datetime(year=2021, month=1, day=1),
source=ProtocolSource(
directory=Path("/dev/null"),
main_file=Path("/dev/null/abc.json"),
config=JsonProtocolConfig(schema_version=123),
files=[],
metadata={},
robot_type=robot_type,
content_hash="abc123",
),
protocol_key="dummy-data-111",
)

raised_exception = Exception("You got me!!")

error_occurrence = pe_errors.ErrorOccurrence.construct(
id="internal-error",
createdAt=datetime(year=2023, month=3, day=3),
errorType="EnumeratedError",
detail="You got me!!",
)

enumerated_error = EnumeratedError(
code=ErrorCodes.GENERAL_ERROR,
message="You got me!!",
)

json_runner = decoy.mock(cls=protocol_runner.JsonRunner)

decoy.when(
await protocol_runner.create_simulating_runner(
robot_type=robot_type,
protocol_config=JsonProtocolConfig(schema_version=123),
)
).then_return(json_runner)

decoy.when(
await json_runner.run(
deck_configuration=[], protocol_source=protocol_resource.source
)
).then_raise(raised_exception)

decoy.when(em.map_unexpected_error(error=raised_exception)).then_return(
enumerated_error
)

decoy.when(datetime_helper.utc_now()).then_return(
datetime(year=2023, month=3, day=3)
)

await subject.analyze(
protocol_resource=protocol_resource,
analysis_id="analysis-id",
)

decoy.verify(
await analysis_store.update(
analysis_id="analysis-id",
robot_type=robot_type,
commands=[],
labware=[],
modules=[],
pipettes=[],
errors=[error_occurrence],
liquids=[],
),
)
Loading