-
Notifications
You must be signed in to change notification settings - Fork 22
fix(testing): handle workflow failure states correctly #682
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,7 @@ | |
| ResourceNotFoundException, | ||
| ) | ||
| from aws_durable_execution_sdk_python_testing.executor import Executor | ||
| from aws_durable_execution_sdk_python_testing.execution import ExecutionStatus | ||
| from aws_durable_execution_sdk_python_testing.invoker import ( | ||
| InProcessInvoker, | ||
| LambdaInvoker, | ||
|
|
@@ -498,6 +499,7 @@ class DurableFunctionTestResult: | |
| operations: list[Operation] | ||
| result: OperationPayload | None = None | ||
| error: ErrorObject | None = None | ||
| execution_status: ExecutionStatus | None = None | ||
|
|
||
| @classmethod | ||
| def create(cls, execution: Execution) -> DurableFunctionTestResult: | ||
|
|
@@ -513,12 +515,16 @@ def create(cls, execution: Execution) -> DurableFunctionTestResult: | |
| if execution.result is None: | ||
| msg: str = "Execution result must exist to create test result." | ||
| raise DurableFunctionsTestError(msg) | ||
| if execution.close_status is None: | ||
| msg_status: str = "Execution close status must exist to create test result." | ||
| raise DurableFunctionsTestError(msg_status) | ||
|
|
||
| return cls( | ||
| status=execution.result.status, | ||
| operations=operations, | ||
| result=execution.result.result, | ||
| error=execution.result.error, | ||
| execution_status=execution.close_status, | ||
| ) | ||
|
|
||
| @classmethod | ||
|
|
@@ -541,6 +547,16 @@ def from_execution_history( | |
| ) | ||
| status = InvocationStatus.FAILED | ||
|
|
||
| # Map overall execution status string separately from invocation status. | ||
| try: | ||
| execution_status = ExecutionStatus[execution_response.status] | ||
| except KeyError: | ||
| logger.warning( | ||
| "Unknown execution status: %s, defaulting to FAILED", | ||
| execution_response.status, | ||
| ) | ||
| execution_status = ExecutionStatus.FAILED | ||
|
|
||
| # Convert Events to Operations - group by operation_id and merge | ||
| try: | ||
| svc_operations = events_to_operations(history_response.events) | ||
|
|
@@ -561,6 +577,7 @@ def from_execution_history( | |
| operations=operations, | ||
| result=execution_response.result, | ||
| error=execution_response.error, | ||
| execution_status=execution_status, | ||
| ) | ||
|
|
||
| def get_operation_by_name(self, name: str) -> Operation: | ||
|
|
@@ -1185,7 +1202,7 @@ def _wait_for_completion( | |
| if execution.status == "FAILED": | ||
| logger.warning("Execution failed") | ||
| return execution | ||
| if execution.status in ["TIMED_OUT", "ABORTED"]: | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think "ABORTED" is a valid execution status. Could not find any references to it in other SDKs. The API documentation also does not list "ABORTED" as being a valid execution status. I think it should instead be "STOPPED", as that specific status is not covered in this function. I noticed this when implementing a new "execution_status" field on the test result class and my AI coding agent flagged this to me. |
||
| if execution.status in ["TIMED_OUT", "STOPPED"]: | ||
| logger.warning("Execution terminated: %s", execution.status) | ||
| return execution | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """End-to-end child context failure handling through the test runner.""" | ||
|
|
||
| import json | ||
| from typing import Any | ||
|
|
||
| from aws_durable_execution_sdk_python.config import StepConfig | ||
| from aws_durable_execution_sdk_python.context import ( | ||
| DurableContext, | ||
| durable_step, | ||
| durable_with_child_context, | ||
| ) | ||
| from aws_durable_execution_sdk_python.execution import durable_execution | ||
| from aws_durable_execution_sdk_python.lambda_service import ( | ||
| InvocationStatus, | ||
| OperationStatus, | ||
| ) | ||
| from aws_durable_execution_sdk_python.retries import RetryPresets | ||
| from aws_durable_execution_sdk_python.types import StepContext | ||
|
|
||
| from aws_durable_execution_sdk_python_testing.runner import ( | ||
| ContextOperation, | ||
| DurableFunctionTestResult, | ||
| DurableFunctionTestRunner, | ||
| ) | ||
|
|
||
|
|
||
| def test_caught_child_context_failure_does_not_fail_root_execution() -> None: | ||
| @durable_step | ||
| def failing_step(step_context: StepContext) -> str: # noqa: ARG001 | ||
| msg = "Child step failed" | ||
| raise RuntimeError(msg) | ||
|
|
||
| @durable_with_child_context | ||
| def failing_child(ctx: DurableContext) -> str: | ||
| return ctx.step( | ||
| failing_step(), | ||
| config=StepConfig(retry_strategy=RetryPresets.none()), | ||
| ) | ||
|
|
||
| @durable_step | ||
| def recovery_step(step_context: StepContext, value: str) -> str: # noqa: ARG001 | ||
| return value | ||
|
|
||
| @durable_execution | ||
| def handler(event: Any, context: DurableContext) -> str: # noqa: ARG001 | ||
| try: | ||
| context.run_in_child_context(failing_child(), name="failing-child") | ||
| except Exception: | ||
| pass | ||
|
|
||
| return context.step(recovery_step("handled")) | ||
|
|
||
| with DurableFunctionTestRunner(handler=handler, execution_timeout=10) as runner: | ||
| result: DurableFunctionTestResult = runner.run(input="input str") | ||
|
|
||
| assert result.status is InvocationStatus.SUCCEEDED | ||
| assert result.result == json.dumps("handled") | ||
|
|
||
| child_op: ContextOperation = result.get_context("failing-child") | ||
| assert child_op.status is OperationStatus.FAILED | ||
| assert child_op.error is not None | ||
| assert child_op.error.message is not None | ||
| assert "Child step failed" in child_op.error.message |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| """End-to-end failed invocation handling through the test runner.""" | ||
|
|
||
| from typing import Any | ||
|
|
||
| from aws_durable_execution_sdk_python.execution import InvocationStatus | ||
|
|
||
| from aws_durable_execution_sdk_python_testing.execution import ExecutionStatus | ||
| from aws_durable_execution_sdk_python_testing.runner import ( | ||
| DurableFunctionTestResult, | ||
| DurableFunctionTestRunner, | ||
| ) | ||
|
|
||
|
|
||
| def test_failed_invocation_without_error_sets_execution_status() -> None: | ||
| def handler(event: Any, context: Any) -> dict[str, str]: # noqa: ARG001 | ||
| return {"Status": "FAILED"} | ||
|
|
||
| with DurableFunctionTestRunner(handler=handler, execution_timeout=10) as runner: | ||
| execution_arn = runner.run_async(input="input str") | ||
| result: DurableFunctionTestResult = runner.wait_for_result( | ||
| execution_arn, timeout=10 | ||
| ) | ||
|
|
||
| assert result.status is InvocationStatus.FAILED | ||
| assert result.error is None | ||
| assert result.execution_status is ExecutionStatus.FAILED |
Uh oh!
There was an error while loading. Please reload this page.