Skip to content
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
5 changes: 5 additions & 0 deletions handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ def handle_muEd_command(event: JsonType, command: str) -> HandlerResponse:
}
return wrap_muEd_response(error, event, 400)

except commands.InvalidSubmissionError as e:
detail = str(e) if str(e) else repr(e)
error = {"title": "Unprocessable submission", "message": detail, "code": ErrorCode.VALIDATION_ERROR}
return wrap_muEd_response(error, event, 422)

except EvaluationException as e:
detail = str(e) if str(e) else repr(e)
error = {"title": "Internal server error", "message": detail, "code": ErrorCode.INTERNAL_ERROR}
Expand Down
26 changes: 26 additions & 0 deletions schemas/muEd/openapi-v0_1_0.yml
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ paths:
$ref: '#/components/responses/403-Forbidden'
'406':
$ref: '#/components/responses/406-VersionNotSupported'
'422':
$ref: '#/components/responses/422-UnprocessableSubmission'
'500':
$ref: '#/components/responses/500-InternalError'
'501':
Expand Down Expand Up @@ -1778,6 +1780,30 @@ components:
requestedVersion: '0.0'
supportedVersions:
- 0.1.0
422-UnprocessableSubmission:
description: The request is valid, but the evaluation function cannot process the submission content (e.g. an unparseable expression).
headers:
X-Request-Id:
description: Request id for tracing this request across services.
schema:
type: string
X-Api-Version:
description: The API version that was used to serve this response.
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
unprocessableSubmission:
summary: Example unprocessable submission error
value:
title: Unprocessable submission
message: 'Failed to parse SymPy expression: A/(w*'
code: VALIDATION_ERROR
trace: null
details: null
500-InternalError:
description: Internal server error.
headers:
Expand Down
72 changes: 72 additions & 0 deletions tests/mued_handling_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,78 @@ def test_presubmission_disabled_runs_normal_evaluation(self):
self.assertIn("awardedPoints", body[0])
self.assertNotIn("preSubmissionFeedback", body[0])

def test_preview_value_error_returns_unprocessable(self):
def preview_function(response, params):
raise ValueError(f"Failed to parse SymPy expression: {response}")

commands.preview_function = preview_function
event = {
"path": "/evaluate",
"body": {
"submission": {"type": "MATH", "content": {"expression": "A/(w*"}},
"preSubmissionFeedback": {"enabled": True},
},
}

response = handler(event)

self.assertEqual(response["statusCode"], 422)
body = json.loads(response["body"])
self.assertEqual(body["code"], "VALIDATION_ERROR")
self.assertEqual(body["message"], "Failed to parse SymPy expression: A/(w*")

def test_preview_unexpected_error_returns_internal_error(self):
def preview_function(response, params):
raise RuntimeError("boom")

commands.preview_function = preview_function
event = {
"path": "/evaluate",
"body": {
"submission": {"type": "MATH", "content": {"expression": "x+1"}},
"preSubmissionFeedback": {"enabled": True},
},
}

response = handler(event)

self.assertEqual(response["statusCode"], 500)
body = json.loads(response["body"])
self.assertEqual(body["code"], "INTERNAL_ERROR")

def test_evaluation_value_error_returns_unprocessable(self):
def evaluation_function(response, answer, params):
raise ValueError(f"Failed to parse SymPy expression: {response}")

commands.evaluation_function = evaluation_function
event = {
"path": "/evaluate",
"body": {"submission": {"type": "MATH", "content": {"expression": "A/(w*"}}},
}

response = handler(event)

self.assertEqual(response["statusCode"], 422)
body = json.loads(response["body"])
self.assertEqual(body["code"], "VALIDATION_ERROR")
self.assertEqual(body["message"], "Failed to parse SymPy expression: A/(w*")

def test_evaluation_unexpected_error_returns_internal_error(self):
def evaluation_function(response, answer, params):
raise RuntimeError("boom")

commands.evaluation_function = evaluation_function
event = {
"path": "/evaluate",
"body": {"submission": {"type": "MATH", "content": {"expression": "x+1"}}},
}

response = handler(event)

self.assertEqual(response["statusCode"], 500)
body = json.loads(response["body"])
self.assertEqual(body["code"], "INTERNAL_ERROR")


class TestMuEdPreviewExtraction(unittest.TestCase):
def setUp(self) -> None:
Expand Down
14 changes: 12 additions & 2 deletions tools/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def preview_function(response: Any, params: Any) -> Dict:
)


class InvalidSubmissionError(Exception):
"""Raised when the evaluation function cannot process the submitted response."""


class CaseWarning(TypedDict, total=False):
"""Dictionary for reporting issues when testing cases"""

Expand Down Expand Up @@ -184,7 +188,10 @@ def _extract_muEd_submission(body: JsonType):

def _run_muEd_preview(body: JsonType) -> List[Dict]:
response, params, _ = _extract_muEd_submission(body)
preview_result = preview_function(response, params)
try:
preview_result = preview_function(response, params)
except ValueError as e:
raise InvalidSubmissionError(str(e)) from e
return [{"preSubmissionFeedback": preview_result.get("preview", {})}]


Expand All @@ -200,7 +207,10 @@ def _run_muEd_evaluation(body: JsonType) -> List[Dict]:
else:
answer = None

result = _run_evaluation(response, answer, params)
try:
result = _run_evaluation(response, answer, params)
except ValueError as e:
raise InvalidSubmissionError(str(e)) from e

is_correct = result.get("is_correct") or 0
feedback_text = result.get("feedback", "")
Expand Down
Loading