From 2aa9be0750ddea694ac3a30d1b3e074ba23d1339 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:25:56 +0100 Subject: [PATCH 1/4] tbc --- .../payloadbinding/payload_binding.py | 23 +-- .../common/string/string_expression.py | 66 +++++- .../asl/component/eval_component.py | 6 + .../asl/component/state/state.py | 6 +- .../stepfunctions/asl/eval/environment.py | 22 +- .../stepfunctions/asl/eval/program_state.py | 23 ++- .../stepfunctions/asl/utils/json_path.py | 10 +- .../testing/pytest/stepfunctions/fixtures.py | 2 +- .../testing/pytest/stepfunctions/utils.py | 2 +- .../scenarios/scenarios_templates.py | 10 + .../invalid_jsonpath_in_errorpath.json5 | 16 ++ ..._jsonpath_in_string_expr_contextpath.json5 | 12 ++ ...lid_jsonpath_in_string_expr_jsonpath.json5 | 12 ++ .../v2/scenarios/test_base_scenarios.py | 39 ++++ .../test_base_scenarios.snapshot.json | 189 ++++++++++++++++++ .../test_base_scenarios.validation.json | 9 + 16 files changed, 411 insertions(+), 36 deletions(-) create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_errorpath.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_string_expr_contextpath.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_string_expr_jsonpath.json5 diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding.py index 0c6fc8ffe5bba..9aa321aa21f4d 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding.py @@ -16,12 +16,11 @@ PayloadValue, ) from localstack.services.stepfunctions.asl.component.common.string.string_expression import ( + NoSuchJsonPathError, StringExpressionSimple, - StringJsonPath, ) from localstack.services.stepfunctions.asl.eval.environment import Environment from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails -from localstack.services.stepfunctions.asl.utils.encoding import to_json_str class PayloadBinding(PayloadValue, abc.ABC): @@ -50,25 +49,23 @@ def __init__(self, field: str, string_expression_simple: StringExpressionSimple) def _eval_val(self, env: Environment) -> Any: try: self.string_expression_simple.eval(env=env) - except RuntimeError as runtime_error: - if isinstance(self.string_expression_simple, StringJsonPath): - input_value_str = ( - to_json_str(env.stack[1]) if env.stack else "" - ) - failure_event = FailureEvent( + except NoSuchJsonPathError as error: + cause = ( + f"The JSONPath '{error.json_path}' specified for the field '{self.field}.$' " + f"could not be found in the input '{error.data}'" + ) + raise FailureEventException( + failure_event=FailureEvent( env=env, error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), event_type=HistoryEventType.TaskFailed, event_details=EventDetails( taskFailedEventDetails=TaskFailedEventDetails( - error=StatesErrorNameType.StatesRuntime.to_name(), - cause=f"The JSONPath {self.string_expression_simple.literal_value} specified for the field {self.field}.$ could not be found in the input {input_value_str}", + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause ) ), ) - raise FailureEventException(failure_event=failure_event) - else: - raise runtime_error + ) value = env.stack.pop() return value diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py index 82f0381ba1ace..9fff2fa73d92e 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py @@ -1,13 +1,26 @@ import abc import copy -from typing import Any, Final +from typing import Any, Final, Optional +from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails +from localstack.services.events.utils import to_json_str +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) from localstack.services.stepfunctions.asl.component.common.query_language import QueryLanguageMode from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent from localstack.services.stepfunctions.asl.component.intrinsic.jsonata import ( get_intrinsic_functions_declarations, ) from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails from localstack.services.stepfunctions.asl.jsonata.jsonata import ( JSONataExpression, VariableDeclarations, @@ -19,17 +32,34 @@ from localstack.services.stepfunctions.asl.jsonata.validations import ( validate_jsonata_expression_output, ) -from localstack.services.stepfunctions.asl.utils.json_path import extract_json +from localstack.services.stepfunctions.asl.utils.json_path import ( + extract_json, +) JSONPATH_ROOT_PATH: Final[str] = "$" +class NoSuchJsonPathError(Exception): + json_path: Final[str] + data: Final[str] + + def __init__(self, json_path: str, data: str): + self.json_path = json_path + self.data = data + + def __str__(self): + return f"The JSONPath '{self.json_path}' could not be found in the input '{self.data}'" + + class StringExpression(EvalComponent, abc.ABC): literal_value: Final[str] def __init__(self, literal_value: str): self.literal_value = literal_value + def _field_name(self) -> Optional[str]: + return None + class StringExpressionSimple(StringExpression, abc.ABC): ... @@ -54,22 +84,50 @@ def _eval_body(self, env: Environment) -> None: if self.json_path == JSONPATH_ROOT_PATH: output_value = input_value else: - output_value = extract_json(self.json_path, input_value) + try: + output_value = extract_json(self.json_path, input_value) + except ValueError: + input_value_json_str = to_json_str(input_value) + if env.next_field_name is not None: + cause = ( + f"The JSONPath '{self.json_path}' specified for the field '{env.next_field_name}' " + f"could not be found in the input '{input_value_json_str}'" + ) + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), + ) + ) + raise NoSuchJsonPathError(self.json_path, input_value_json_str) # TODO: introduce copy on write approach env.stack.append(copy.deepcopy(output_value)) class StringContextPath(StringJsonPath): + context_object_path: Final[str] + def __init__(self, context_object_path: str): json_path = context_object_path[1:] super().__init__(json_path=json_path) + self.context_object_path = context_object_path def _eval_body(self, env: Environment) -> None: input_value = env.states.context_object.context_object_data if self.json_path == JSONPATH_ROOT_PATH: output_value = input_value else: - output_value = extract_json(self.json_path, input_value) + try: + output_value = extract_json(self.json_path, input_value) + except ValueError: + input_value_json_str = to_json_str(input_value) + raise NoSuchJsonPathError(self.context_object_path, input_value_json_str) # TODO: introduce copy on write approach env.stack.append(copy.deepcopy(output_value)) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/eval_component.py b/localstack-core/localstack/services/stepfunctions/asl/component/eval_component.py index 315f0f5b02029..cd7940208f5cc 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/eval_component.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/eval_component.py @@ -65,6 +65,9 @@ def eval(self, env: Environment) -> None: if env.is_running(): self._log_evaluation_step("Computing") try: + field_name = self._field_name() + if field_name is not None: + env.next_field_name = field_name self._eval_body(env) except FailureEventException as failure_event_exception: self._log_failure_event_exception(failure_event_exception=failure_event_exception) @@ -78,3 +81,6 @@ def eval(self, env: Environment) -> None: @abc.abstractmethod def _eval_body(self, env: Environment) -> None: raise NotImplementedError() + + def _field_name(self) -> Optional[str]: + return self.__class__.__name__ diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py index 5b64a3b2c1e8d..f5e1735cad206 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py @@ -40,6 +40,7 @@ ) from localstack.services.stepfunctions.asl.component.common.string.string_expression import ( JSONPATH_ROOT_PATH, + NoSuchJsonPathError, StringJsonPath, ) from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent @@ -208,7 +209,10 @@ def _eval_body(self, env: Environment) -> None: env.stack.append(env.states.get_input()) # Exec the state's logic. - self._eval_state(env) + try: + self._eval_state(env) + except NoSuchJsonPathError: + pass # if not isinstance(env.program_state(), ProgramRunning): return diff --git a/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py b/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py index 29cd95559cd73..61e2f295b7a69 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py +++ b/localstack-core/localstack/services/stepfunctions/asl/eval/environment.py @@ -149,8 +149,9 @@ def as_inner_frame_of( @property def next_state_name(self) -> Optional[str]: next_state_name: Optional[str] = None - if isinstance(self._program_state, ProgramRunning): - next_state_name = self._program_state.next_state_name + program_state = self._program_state + if isinstance(program_state, ProgramRunning): + next_state_name = program_state.next_state_name return next_state_name @next_state_name.setter @@ -165,6 +166,23 @@ def next_state_name(self, next_state_name: str) -> None: f"Could not set NextState value when in state '{type(self._program_state)}'." ) + @property + def next_field_name(self) -> Optional[str]: + next_field_name: Optional[str] = None + program_state = self._program_state + if isinstance(program_state, ProgramRunning): + next_field_name = program_state.next_field_name + return next_field_name + + @next_field_name.setter + def next_field_name(self, next_field_name: str) -> None: + if isinstance(self._program_state, ProgramRunning): + self._program_state.next_field_name = next_field_name + else: + raise RuntimeError( + f"Could not set NextField value when in state '{type(self._program_state)}'." + ) + def program_state(self) -> ProgramState: return copy.deepcopy(self._program_state) diff --git a/localstack-core/localstack/services/stepfunctions/asl/eval/program_state.py b/localstack-core/localstack/services/stepfunctions/asl/eval/program_state.py index 39dd31e316a0c..00f3af00cb82f 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/eval/program_state.py +++ b/localstack-core/localstack/services/stepfunctions/asl/eval/program_state.py @@ -20,9 +20,13 @@ def __init__(self, stop_date: Timestamp, error: Optional[str], cause: Optional[s class ProgramRunning(ProgramState): + _next_state_name: Optional[str] + _next_field_name: Optional[str] + def __init__(self): super().__init__() - self._next_state_name: Optional[str] = None + self._next_state_name = None + self._next_field_name = None @property def next_state_name(self) -> str: @@ -33,14 +37,19 @@ def next_state_name(self) -> str: @next_state_name.setter def next_state_name(self, next_state_name) -> None: - if not self._validate_next_state_name(next_state_name): - raise ValueError(f"No such NextState '{next_state_name}'.") self._next_state_name = next_state_name + self._next_field_name = None + + @property + def next_field_name(self) -> str: + return self._next_field_name - @staticmethod - def _validate_next_state_name(next_state_name: Optional[str]) -> bool: - # TODO. - return bool(next_state_name) + @next_field_name.setter + def next_field_name(self, next_field_name) -> None: + next_state_name = self._next_state_name + if next_state_name is None: + raise RuntimeError("Could not set NextField from uninitialised ProgramState.") + self._next_field_name = next_field_name class ProgramError(ProgramState): diff --git a/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py b/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py index 565ccdf398ffd..010c91720d0a4 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py +++ b/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py @@ -1,12 +1,10 @@ import json import re -from typing import Final +from typing import Any, Final from jsonpath_ng.ext import parse from jsonpath_ng.jsonpath import Index -from localstack.services.stepfunctions.asl.utils.encoding import to_json_str - _PATTERN_SINGLETON_ARRAY_ACCESS_OUTPUT: Final[str] = r"\[\d+\]$" @@ -15,14 +13,12 @@ def _is_singleton_array_access(path: str) -> bool: return bool(re.search(_PATTERN_SINGLETON_ARRAY_ACCESS_OUTPUT, path)) -def extract_json(path: str, data: json) -> json: +def extract_json(path: str, data: Any) -> json: input_expr = parse(path) matches = input_expr.find(data) if not matches: - raise RuntimeError( - f"The JSONPath {path} could not be found in the input {to_json_str(data)}" - ) + raise ValueError(f"The JSONPath {path} could not be found in the input") if len(matches) > 1 or isinstance(matches[0].path, Index): value = [match.value for match in matches] diff --git a/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py b/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py index e1152072da49d..3ec1635dcfa5c 100644 --- a/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py +++ b/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py @@ -178,7 +178,7 @@ def _create(target_aws_client): "Statement": [ { "Effect": "Allow", - "Action": ["*"], + "Action": "s3:ListBuckets", "Resource": ["*"], } ], diff --git a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py index ed34cbc6ea4e7..ad72bdf00f09f 100644 --- a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py +++ b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py @@ -395,7 +395,7 @@ def launch_and_record_execution( map_run_arns = [map_run_arns] for i, map_run_arn in enumerate(list(set(map_run_arns))): sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_map_run_arn(map_run_arn, i)) - except RuntimeError: + except ValueError: # No mapRunArns pass diff --git a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py index b64f4bf0ed874..4acd825041438 100644 --- a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py +++ b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py @@ -247,3 +247,13 @@ class ScenariosTemplate(TemplateLoader): RAISE_FAILURE_CAUSE_JSONATA: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/fail_cause_jsonata.json5" ) + + INVALID_JSONPATH_IN_ERRORPATH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/invalid_jsonpath_in_errorpath.json5" + ) + INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/invalid_jsonpath_in_string_expr_jsonpath.json5" + ) + INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/invalid_jsonpath_in_string_expr_contextpath.json5" + ) diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_errorpath.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_errorpath.json5 new file mode 100644 index 0000000000000..21654f7a90da2 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_errorpath.json5 @@ -0,0 +1,16 @@ +{ + "StartAt": "pass", + "States": { + "pass": { + "Type": "Pass", + "Next": "fail", + "Parameters": { + "Error": "error-value", + } + }, + "fail": { + "Type": "Fail", + "ErrorPath": "$.ErrorX", + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_string_expr_contextpath.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_string_expr_contextpath.json5 new file mode 100644 index 0000000000000..ba6fced5d5a87 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_string_expr_contextpath.json5 @@ -0,0 +1,12 @@ +{ + "StartAt": "pass", + "States": { + "pass": { + "Type": "Pass", + "Parameters": { + "value.$": "$$.Execution.Input.no_such_jsonpath", + }, + "End": true + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_string_expr_jsonpath.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_string_expr_jsonpath.json5 new file mode 100644 index 0000000000000..ce2cab38fa13f --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_string_expr_jsonpath.json5 @@ -0,0 +1,12 @@ +{ + "StartAt": "pass", + "States": { + "pass": { + "Type": "Pass", + "Parameters": { + "value.$": "$.no_such_jsonpath", + }, + "End": true + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py index c5f3a66d4fe93..1ec79596384ae 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py @@ -2494,3 +2494,42 @@ def test_fail_cause_jsonata( definition, exec_input, ) + + @markers.aws.validated + @pytest.mark.parametrize( + "template", + [ + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_ERRORPATH), + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH), + pytest.param( + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH), + marks=pytest.mark.skipif( + condition=not is_aws_cloud(), + reason="serialisation of the context object bindings is unordered", + ), + ), + ], + ids=[ + "INVALID_JSONPATH_IN_ERRORPATH", + "INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH", + "INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH", + ], + ) + def test_invalid_jsonpath( + self, + aws_client, + create_state_machine_iam_role, + create_state_machine, + snapshot, + template, + ): + definition = json.dumps(template) + exec_input = json.dumps({"int-literal": 0}) + create_and_record_execution( + aws_client, + create_state_machine_iam_role, + create_state_machine, + snapshot, + definition, + exec_input, + ) diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json index f9023e4f1e2dc..86a2f5369be95 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json @@ -25045,5 +25045,194 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_ERRORPATH]": { + "recorded-date": "30-12-2024, 14:57:21", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "pass", + "output": { + "Error": "error-value" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 4, + "previousEventId": 3, + "stateEnteredEventDetails": { + "input": { + "Error": "error-value" + }, + "inputDetails": { + "truncated": false + }, + "name": "fail" + }, + "timestamp": "timestamp", + "type": "FailStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'fail' (entered at the event id #4). The JSONPath '$.ErrorX' specified for the field 'ErrorPath' could not be found in the input '{\"Error\":\"error-value\"}'", + "error": "States.Runtime" + }, + "id": 5, + "previousEventId": 4, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH]": { + "recorded-date": "30-12-2024, 14:57:36", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'pass' (entered at the event id #2). The JSONPath '$.no_such_jsonpath' specified for the field 'value.$' could not be found in the input '{\"int-literal\": 0}'", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH]": { + "recorded-date": "30-12-2024, 14:57:51", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'pass' (entered at the event id #2). The JSONPath '$$.Execution.Input.no_such_jsonpath' specified for the field 'value.$' could not be found in the input '{\"Execution\":{\"Id\":\"arn::states::111111111111:execution::\",\"Input\":{\"int-literal\":0},\"StartTime\":\"date\",\"Name\":\"\",\"RoleArn\":\"snf_role_arn\",\"RedriveCount\":0},\"StateMachine\":{\"Id\":\"arn::states::111111111111:stateMachine:\",\"Name\":\"\"},\"State\":{\"Name\":\"pass\",\"EnteredTime\":\"date\"}}'", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json index 3ae7725ca78c9..8c78c4e8bcdf4 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json @@ -38,6 +38,15 @@ "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_fail_error_jsonata": { "last_validated_date": "2024-11-13T16:35:39+00:00" }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_ERRORPATH]": { + "last_validated_date": "2024-12-30T14:57:21+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH]": { + "last_validated_date": "2024-12-30T14:57:51+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH]": { + "last_validated_date": "2024-12-30T14:57:36+00:00" + }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_lambda_empty_retry": { "last_validated_date": "2023-11-23T17:08:38+00:00" }, From 48d5c6d654ff9bf95a822a0c990ae4d3d29cfa48 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:13:21 +0100 Subject: [PATCH 2/4] improvements, but see custom jsonpath error scenarios failure in scenario_base and logs tests --- .../jsonata/jsonata_template_binding.py | 5 +- .../payloadbinding/payload_binding.py | 42 +-- .../common/string/string_expression.py | 66 ++-- .../asl/component/state/state.py | 8 +- .../stepfunctions/asl/utils/json_path.py | 32 +- .../testing/pytest/stepfunctions/fixtures.py | 2 +- .../testing/pytest/stepfunctions/utils.py | 4 +- .../scenarios/scenarios_templates.py | 15 + .../invalid_jsonpath_in_causepath.json5 | 17 + ...lid_jsonpath_in_heartbeatsecondspath.json5 | 12 + .../invalid_jsonpath_in_inputpath.json5 | 10 + .../invalid_jsonpath_in_outputpath.json5 | 10 + ...valid_jsonpath_in_timeoutsecondspath.json5 | 12 + .../v2/scenarios/test_base_scenarios.py | 22 +- .../test_base_scenarios.snapshot.json | 307 +++++++++++++++++- .../test_base_scenarios.validation.json | 21 +- 16 files changed, 487 insertions(+), 98 deletions(-) create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_causepath.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_heartbeatsecondspath.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_inputpath.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_outputpath.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_timeoutsecondspath.json5 diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/jsonata/jsonata_template_binding.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/jsonata/jsonata_template_binding.py index ac88b48c9c773..3833f14c0abdc 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/jsonata/jsonata_template_binding.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/jsonata/jsonata_template_binding.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Final +from typing import Final, Optional from localstack.services.stepfunctions.asl.component.common.jsonata.jsonata_template_value import ( JSONataTemplateValue, @@ -17,6 +17,9 @@ def __init__(self, identifier: str, value: JSONataTemplateValue): self.identifier = identifier self.value = value + def _field_name(self) -> Optional[str]: + return self.identifier + def _eval_body(self, env: Environment) -> None: binding_container: dict = env.stack.pop() self.value.eval(env=env) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding.py index 9aa321aa21f4d..1b7d7fb527634 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding.py @@ -1,26 +1,13 @@ import abc -from typing import Any, Final +from typing import Any, Final, Optional -from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails -from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( - FailureEvent, - FailureEventException, -) -from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( - StatesErrorName, -) -from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( - StatesErrorNameType, -) from localstack.services.stepfunctions.asl.component.common.payload.payloadvalue.payload_value import ( PayloadValue, ) from localstack.services.stepfunctions.asl.component.common.string.string_expression import ( - NoSuchJsonPathError, StringExpressionSimple, ) from localstack.services.stepfunctions.asl.eval.environment import Environment -from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails class PayloadBinding(PayloadValue, abc.ABC): @@ -29,6 +16,9 @@ class PayloadBinding(PayloadValue, abc.ABC): def __init__(self, field: str): self.field = field + def _field_name(self) -> Optional[str]: + return self.field + @abc.abstractmethod def _eval_val(self, env: Environment) -> Any: ... @@ -46,27 +36,11 @@ def __init__(self, field: str, string_expression_simple: StringExpressionSimple) super().__init__(field=field) self.string_expression_simple = string_expression_simple - def _eval_val(self, env: Environment) -> Any: - try: - self.string_expression_simple.eval(env=env) - except NoSuchJsonPathError as error: - cause = ( - f"The JSONPath '{error.json_path}' specified for the field '{self.field}.$' " - f"could not be found in the input '{error.data}'" - ) - raise FailureEventException( - failure_event=FailureEvent( - env=env, - error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), - event_type=HistoryEventType.TaskFailed, - event_details=EventDetails( - taskFailedEventDetails=TaskFailedEventDetails( - error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause - ) - ), - ) - ) + def _field_name(self) -> Optional[str]: + return f"{self.field}.$" + def _eval_val(self, env: Environment) -> Any: + self.string_expression_simple.eval(env=env) value = env.stack.pop() return value diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py index 9fff2fa73d92e..df89fc5600e22 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py @@ -33,24 +33,13 @@ validate_jsonata_expression_output, ) from localstack.services.stepfunctions.asl.utils.json_path import ( + NoSuchJsonPathError, extract_json, ) JSONPATH_ROOT_PATH: Final[str] = "$" -class NoSuchJsonPathError(Exception): - json_path: Final[str] - data: Final[str] - - def __init__(self, json_path: str, data: str): - self.json_path = json_path - self.data = data - - def __str__(self): - return f"The JSONPath '{self.json_path}' could not be found in the input '{self.data}'" - - class StringExpression(EvalComponent, abc.ABC): literal_value: Final[str] @@ -86,26 +75,24 @@ def _eval_body(self, env: Environment) -> None: else: try: output_value = extract_json(self.json_path, input_value) - except ValueError: + except NoSuchJsonPathError: input_value_json_str = to_json_str(input_value) - if env.next_field_name is not None: - cause = ( - f"The JSONPath '{self.json_path}' specified for the field '{env.next_field_name}' " - f"could not be found in the input '{input_value_json_str}'" - ) - raise FailureEventException( - failure_event=FailureEvent( - env=env, - error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), - event_type=HistoryEventType.TaskFailed, - event_details=EventDetails( - taskFailedEventDetails=TaskFailedEventDetails( - error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause - ) - ), - ) + cause = ( + f"The JSONPath '{self.json_path}' specified for the field '{env.next_field_name}' " + f"could not be found in the input '{input_value_json_str}'" + ) + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), ) - raise NoSuchJsonPathError(self.json_path, input_value_json_str) + ) # TODO: introduce copy on write approach env.stack.append(copy.deepcopy(output_value)) @@ -125,9 +112,24 @@ def _eval_body(self, env: Environment) -> None: else: try: output_value = extract_json(self.json_path, input_value) - except ValueError: + except NoSuchJsonPathError: input_value_json_str = to_json_str(input_value) - raise NoSuchJsonPathError(self.context_object_path, input_value_json_str) + cause = ( + f"The JSONPath '${self.json_path}' specified for the field '{env.next_field_name}' " + f"could not be found in the input '{input_value_json_str}'" + ) + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), + ) + ) # TODO: introduce copy on write approach env.stack.append(copy.deepcopy(output_value)) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py index f5e1735cad206..37964efa9cf9a 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py @@ -40,7 +40,6 @@ ) from localstack.services.stepfunctions.asl.component.common.string.string_expression import ( JSONPATH_ROOT_PATH, - NoSuchJsonPathError, StringJsonPath, ) from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent @@ -209,11 +208,8 @@ def _eval_body(self, env: Environment) -> None: env.stack.append(env.states.get_input()) # Exec the state's logic. - try: - self._eval_state(env) - except NoSuchJsonPathError: - pass - # + self._eval_state(env) + if not isinstance(env.program_state(), ProgramRunning): return diff --git a/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py b/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py index 010c91720d0a4..5345d53a225cc 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py +++ b/localstack-core/localstack/services/stepfunctions/asl/utils/json_path.py @@ -1,10 +1,11 @@ -import json import re -from typing import Any, Final +from typing import Any, Final, Optional from jsonpath_ng.ext import parse from jsonpath_ng.jsonpath import Index +from localstack.services.events.utils import to_json_str + _PATTERN_SINGLETON_ARRAY_ACCESS_OUTPUT: Final[str] = r"\[\d+\]$" @@ -13,12 +14,35 @@ def _is_singleton_array_access(path: str) -> bool: return bool(re.search(_PATTERN_SINGLETON_ARRAY_ACCESS_OUTPUT, path)) -def extract_json(path: str, data: Any) -> json: +class NoSuchJsonPathError(Exception): + json_path: Final[str] + data: Final[Any] + _message: Optional[str] + + def __init__(self, json_path: str, data: Any): + self.json_path = json_path + self.data = data + self._message = None + + @property + def message(self) -> str: + if self._message is None: + data_json_str = to_json_str(self.data) + self._message = ( + f"The JSONPath '{self.json_path}' could not be found in the input '{data_json_str}'" + ) + return self._message + + def __str__(self): + return self.message + + +def extract_json(path: str, data: Any) -> Any: input_expr = parse(path) matches = input_expr.find(data) if not matches: - raise ValueError(f"The JSONPath {path} could not be found in the input") + raise NoSuchJsonPathError(json_path=path, data=data) if len(matches) > 1 or isinstance(matches[0].path, Index): value = [match.value for match in matches] diff --git a/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py b/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py index 3ec1635dcfa5c..e1152072da49d 100644 --- a/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py +++ b/localstack-core/localstack/testing/pytest/stepfunctions/fixtures.py @@ -178,7 +178,7 @@ def _create(target_aws_client): "Statement": [ { "Effect": "Allow", - "Action": "s3:ListBuckets", + "Action": ["*"], "Resource": ["*"], } ], diff --git a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py index ad72bdf00f09f..5c76fa5420eff 100644 --- a/localstack-core/localstack/testing/pytest/stepfunctions/utils.py +++ b/localstack-core/localstack/testing/pytest/stepfunctions/utils.py @@ -25,7 +25,7 @@ ) from localstack.services.stepfunctions.asl.eval.event.logging import is_logging_enabled_for from localstack.services.stepfunctions.asl.utils.encoding import to_json_str -from localstack.services.stepfunctions.asl.utils.json_path import extract_json +from localstack.services.stepfunctions.asl.utils.json_path import NoSuchJsonPathError, extract_json from localstack.utils.strings import short_uid from localstack.utils.sync import poll_condition @@ -395,7 +395,7 @@ def launch_and_record_execution( map_run_arns = [map_run_arns] for i, map_run_arn in enumerate(list(set(map_run_arns))): sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_map_run_arn(map_run_arn, i)) - except ValueError: + except NoSuchJsonPathError: # No mapRunArns pass diff --git a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py index 4acd825041438..10bfce69d8a74 100644 --- a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py +++ b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py @@ -251,9 +251,24 @@ class ScenariosTemplate(TemplateLoader): INVALID_JSONPATH_IN_ERRORPATH: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/invalid_jsonpath_in_errorpath.json5" ) + INVALID_JSONPATH_IN_CAUSEPATH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/invalid_jsonpath_in_causepath.json5" + ) INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/invalid_jsonpath_in_string_expr_jsonpath.json5" ) INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/invalid_jsonpath_in_string_expr_contextpath.json5" ) + INVALID_JSONPATH_IN_HEARTBEATSECONDSPATH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/invalid_jsonpath_in_heartbeatsecondspath.json5" + ) + INVALID_JSONPATH_IN_TIMEOUTSECONDSPATH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/invalid_jsonpath_in_timeoutsecondspath.json5" + ) + INVALID_JSONPATH_IN_INPUTPATH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/invalid_jsonpath_in_inputpath.json5" + ) + INVALID_JSONPATH_IN_OUTPUTPATH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/invalid_jsonpath_in_outputpath.json5" + ) diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_causepath.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_causepath.json5 new file mode 100644 index 0000000000000..3ffc36d42b93e --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_causepath.json5 @@ -0,0 +1,17 @@ +{ + "StartAt": "pass", + "States": { + "pass": { + "Type": "Pass", + "Next": "fail", + "Parameters": { + "Error": "error-value", + } + }, + "fail": { + "Type": "Fail", + "ErrorPath": "$.Error", + "CausePath": "$.NoSuchCausePath" + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_heartbeatsecondspath.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_heartbeatsecondspath.json5 new file mode 100644 index 0000000000000..95079504148bd --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_heartbeatsecondspath.json5 @@ -0,0 +1,12 @@ +{ + "StartAt": "task", + "States": { + "task": { + "Type": "Task", + "HeartbeatSecondsPath": "$.NoSuchTimeoutSecondsPath", + "Resource": "arn:aws:states:::aws-sdk:s3:listBuckets.waitForTaskToken", + "Parameters": {}, + "End": true + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_inputpath.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_inputpath.json5 new file mode 100644 index 0000000000000..20a284be00449 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_inputpath.json5 @@ -0,0 +1,10 @@ +{ + "StartAt": "pass", + "States": { + "pass": { + "Type": "Pass", + "InputPath": "$.NoSuchInputPath", + "End": true + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_outputpath.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_outputpath.json5 new file mode 100644 index 0000000000000..50aaa7d51dc5a --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_outputpath.json5 @@ -0,0 +1,10 @@ +{ + "StartAt": "pass", + "States": { + "pass": { + "Type": "Pass", + "OutputPath": "$.NoSuchOutputPath", + "End": true + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_timeoutsecondspath.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_timeoutsecondspath.json5 new file mode 100644 index 0000000000000..3d74ca5685073 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/invalid_jsonpath_in_timeoutsecondspath.json5 @@ -0,0 +1,12 @@ +{ + "StartAt": "task", + "States": { + "task": { + "Type": "Task", + "TimeoutSecondsPath": "$.NoSuchTimeoutSecondsPath", + "Resource": "arn:aws:states:::aws-sdk:s3:listBuckets", + "Parameters": {}, + "End": true + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py index 1ec79596384ae..b8626cb2986f6 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py @@ -2501,18 +2501,22 @@ def test_fail_cause_jsonata( [ ST.load_sfn_template(ST.INVALID_JSONPATH_IN_ERRORPATH), ST.load_sfn_template(ST.INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH), - pytest.param( - ST.load_sfn_template(ST.INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH), - marks=pytest.mark.skipif( - condition=not is_aws_cloud(), - reason="serialisation of the context object bindings is unordered", - ), - ), + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH), + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_CAUSEPATH), + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_INPUTPATH), + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_OUTPUTPATH), + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_TIMEOUTSECONDSPATH), + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_HEARTBEATSECONDSPATH), ], ids=[ "INVALID_JSONPATH_IN_ERRORPATH", "INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH", "INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH", + "ST.INVALID_JSONPATH_IN_CAUSEPATH", + "ST.INVALID_JSONPATH_IN_INPUTPATH", + "ST.INVALID_JSONPATH_IN_OUTPUTPATH", + "ST.INVALID_JSONPATH_IN_TIMEOUTSECONDSPATH", + "ST.INVALID_JSONPATH_IN_HEARTBEATSECONDSPATH", ], ) def test_invalid_jsonpath( @@ -2520,7 +2524,7 @@ def test_invalid_jsonpath( aws_client, create_state_machine_iam_role, create_state_machine, - snapshot, + sfn_snapshot, template, ): definition = json.dumps(template) @@ -2529,7 +2533,7 @@ def test_invalid_jsonpath( aws_client, create_state_machine_iam_role, create_state_machine, - snapshot, + sfn_snapshot, definition, exec_input, ) diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json index 86a2f5369be95..2aecd2a70b0b2 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json @@ -25047,7 +25047,7 @@ } }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_ERRORPATH]": { - "recorded-date": "30-12-2024, 14:57:21", + "recorded-date": "02-01-2025, 13:44:29", "recorded-content": { "get_execution_history": { "events": [ @@ -25113,7 +25113,7 @@ }, { "executionFailedEventDetails": { - "cause": "An error occurred while executing the state 'fail' (entered at the event id #4). The JSONPath '$.ErrorX' specified for the field 'ErrorPath' could not be found in the input '{\"Error\":\"error-value\"}'", + "cause": "An error occurred while executing the state 'fail' (entered at the event id #4). The JSONPath '$.ErrorX' specified for the field 'ErrorPath' could not be found in the input ''", "error": "States.Runtime" }, "id": 5, @@ -25130,7 +25130,7 @@ } }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH]": { - "recorded-date": "30-12-2024, 14:57:36", + "recorded-date": "02-01-2025, 13:44:45", "recorded-content": { "get_execution_history": { "events": [ @@ -25166,7 +25166,7 @@ }, { "executionFailedEventDetails": { - "cause": "An error occurred while executing the state 'pass' (entered at the event id #2). The JSONPath '$.no_such_jsonpath' specified for the field 'value.$' could not be found in the input '{\"int-literal\": 0}'", + "cause": "An error occurred while executing the state 'pass' (entered at the event id #2). The JSONPath '$.no_such_jsonpath' specified for the field 'value.$' could not be found in the input ''", "error": "States.Runtime" }, "id": 3, @@ -25183,7 +25183,7 @@ } }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH]": { - "recorded-date": "30-12-2024, 14:57:51", + "recorded-date": "02-01-2025, 13:45:01", "recorded-content": { "get_execution_history": { "events": [ @@ -25219,7 +25219,302 @@ }, { "executionFailedEventDetails": { - "cause": "An error occurred while executing the state 'pass' (entered at the event id #2). The JSONPath '$$.Execution.Input.no_such_jsonpath' specified for the field 'value.$' could not be found in the input '{\"Execution\":{\"Id\":\"arn::states::111111111111:execution::\",\"Input\":{\"int-literal\":0},\"StartTime\":\"date\",\"Name\":\"\",\"RoleArn\":\"snf_role_arn\",\"RedriveCount\":0},\"StateMachine\":{\"Id\":\"arn::states::111111111111:stateMachine:\",\"Name\":\"\"},\"State\":{\"Name\":\"pass\",\"EnteredTime\":\"date\"}}'", + "cause": "An error occurred while executing the state 'pass' (entered at the event id #2). The JSONPath '$$.Execution.Input.no_such_jsonpath' specified for the field 'value.$' could not be found in the input ''", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_CAUSEPATH]": { + "recorded-date": "02-01-2025, 14:21:03", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "pass", + "output": { + "Error": "error-value" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 4, + "previousEventId": 3, + "stateEnteredEventDetails": { + "input": { + "Error": "error-value" + }, + "inputDetails": { + "truncated": false + }, + "name": "fail" + }, + "timestamp": "timestamp", + "type": "FailStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'fail' (entered at the event id #4). The JSONPath '$.NoSuchCausePath' specified for the field 'CausePath' could not be found in the input ''", + "error": "States.Runtime" + }, + "id": 5, + "previousEventId": 4, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_INPUTPATH]": { + "recorded-date": "02-01-2025, 14:21:19", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'pass' (entered at the event id #2). Invalid path '$.NoSuchInputPath' : No results for path: $['NoSuchInputPath']", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_OUTPUTPATH]": { + "recorded-date": "02-01-2025, 14:21:35", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'pass' (entered at the event id #2). Invalid path '$.NoSuchOutputPath' : No results for path: $['NoSuchOutputPath']", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_TIMEOUTSECONDSPATH]": { + "recorded-date": "02-01-2025, 14:26:32", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "task" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'task' (entered at the event id #2). Invalid path '$.NoSuchTimeoutSecondsPath' : No results for path: $['NoSuchTimeoutSecondsPath']", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_HEARTBEATSECONDSPATH]": { + "recorded-date": "02-01-2025, 14:26:48", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "int-literal": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "task" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'task' (entered at the event id #2). Invalid path '$.NoSuchTimeoutSecondsPath' : No results for path: $['NoSuchTimeoutSecondsPath']", "error": "States.Runtime" }, "id": 3, diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json index 8c78c4e8bcdf4..7c6391ac11287 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json @@ -39,13 +39,28 @@ "last_validated_date": "2024-11-13T16:35:39+00:00" }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_ERRORPATH]": { - "last_validated_date": "2024-12-30T14:57:21+00:00" + "last_validated_date": "2025-01-02T13:44:29+00:00" }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_STRING_EXPR_CONTEXTPATH]": { - "last_validated_date": "2024-12-30T14:57:51+00:00" + "last_validated_date": "2025-01-02T13:45:01+00:00" }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[INVALID_JSONPATH_IN_STRING_EXPR_JSONPATH]": { - "last_validated_date": "2024-12-30T14:57:36+00:00" + "last_validated_date": "2025-01-02T13:44:45+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_CAUSEPATH]": { + "last_validated_date": "2025-01-02T14:21:03+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_HEARTBEATSECONDSPATH]": { + "last_validated_date": "2025-01-02T14:26:48+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_INPUTPATH]": { + "last_validated_date": "2025-01-02T14:21:19+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_OUTPUTPATH]": { + "last_validated_date": "2025-01-02T14:21:35+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_invalid_jsonpath[ST.INVALID_JSONPATH_IN_TIMEOUTSECONDSPATH]": { + "last_validated_date": "2025-01-02T14:26:32+00:00" }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_lambda_empty_retry": { "last_validated_date": "2023-11-23T17:08:38+00:00" From 3bbebd763067a83e74b227204bb3c24840d604e1 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:28:33 +0100 Subject: [PATCH 3/4] custom error messages, minors --- .../asl/component/common/path/input_path.py | 31 ++++++++++++++++- .../asl/component/common/path/output_path.py | 31 ++++++++++++++++- .../common/string/string_expression.py | 21 +----------- .../component/common/timeouts/heartbeat.py | 31 ++++++++++++++++- .../asl/component/common/timeouts/timeout.py | 34 ++++++++++++++++++- .../asl/component/state/state.py | 23 ++++++++++++- .../state_wait/wait_function/seconds_path.py | 23 +++++++++++-- .../stepfunctions/v2/logs/test_logs.py | 2 -- .../v2/scenarios/test_base_scenarios.py | 16 +++++++-- 9 files changed, 181 insertions(+), 31 deletions(-) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/path/input_path.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/path/input_path.py index d15c8035d4615..8c0d4e6cbb4e7 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/path/input_path.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/path/input_path.py @@ -1,11 +1,24 @@ from typing import Final, Optional +from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) from localstack.services.stepfunctions.asl.component.common.string.string_expression import ( StringJsonPath, StringSampler, ) from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.asl.utils.json_path import NoSuchJsonPathError class InputPath(EvalComponent): @@ -21,4 +34,20 @@ def _eval_body(self, env: Environment) -> None: if isinstance(self.string_sampler, StringJsonPath): # JsonPaths are sampled from a given state, hence pass the state's input. env.stack.append(env.states.get_input()) - self.string_sampler.eval(env=env) + try: + self.string_sampler.eval(env=env) + except NoSuchJsonPathError as no_such_json_path_error: + json_path = no_such_json_path_error.json_path + cause = f"Invalid path '{json_path}' : No results for path: $['{json_path[2:]}']" + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), + ) + ) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/path/output_path.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/path/output_path.py index dedeb3055d24e..b40586aa8e716 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/path/output_path.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/path/output_path.py @@ -1,10 +1,23 @@ from typing import Final, Optional +from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) from localstack.services.stepfunctions.asl.component.common.string.string_expression import ( StringSampler, ) from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.asl.utils.json_path import NoSuchJsonPathError class OutputPath(EvalComponent): @@ -17,6 +30,22 @@ def _eval_body(self, env: Environment) -> None: if self.string_sampler is None: env.states.reset(input_value=dict()) return - self.string_sampler.eval(env=env) + try: + self.string_sampler.eval(env=env) + except NoSuchJsonPathError as no_such_json_path_error: + json_path = no_such_json_path_error.json_path + cause = f"Invalid path '{json_path}' : No results for path: $['{json_path[2:]}']" + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), + ) + ) output_value = env.stack.pop() env.states.reset(output_value) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py index df89fc5600e22..3f4be28c7e14c 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/string/string_expression.py @@ -73,26 +73,7 @@ def _eval_body(self, env: Environment) -> None: if self.json_path == JSONPATH_ROOT_PATH: output_value = input_value else: - try: - output_value = extract_json(self.json_path, input_value) - except NoSuchJsonPathError: - input_value_json_str = to_json_str(input_value) - cause = ( - f"The JSONPath '{self.json_path}' specified for the field '{env.next_field_name}' " - f"could not be found in the input '{input_value_json_str}'" - ) - raise FailureEventException( - failure_event=FailureEvent( - env=env, - error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), - event_type=HistoryEventType.TaskFailed, - event_details=EventDetails( - taskFailedEventDetails=TaskFailedEventDetails( - error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause - ) - ), - ) - ) + output_value = extract_json(self.json_path, input_value) # TODO: introduce copy on write approach env.stack.append(copy.deepcopy(output_value)) diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/timeouts/heartbeat.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/timeouts/heartbeat.py index 9a3720be1b345..c268239346079 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/timeouts/heartbeat.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/timeouts/heartbeat.py @@ -1,12 +1,25 @@ import abc from typing import Final +from localstack.aws.api.stepfunctions import ExecutionFailedEventDetails, HistoryEventType +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) from localstack.services.stepfunctions.asl.component.common.string.string_expression import ( StringJSONata, StringSampler, ) from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.asl.utils.json_path import NoSuchJsonPathError class Heartbeat(EvalComponent, abc.ABC): @@ -51,7 +64,23 @@ def __init__(self, string_sampler: StringSampler): self.string_sampler = string_sampler def _eval_seconds(self, env: Environment) -> int: - self.string_sampler.eval(env=env) + try: + self.string_sampler.eval(env=env) + except NoSuchJsonPathError as no_such_json_path_error: + json_path = no_such_json_path_error.json_path + cause = f"Invalid path '{json_path}' : No results for path: $['{json_path[2:]}']" + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.ExecutionFailed, + event_details=EventDetails( + executionFailedEventDetails=ExecutionFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), + ) + ) seconds = env.stack.pop() if not isinstance(seconds, int) and seconds <= 0: raise ValueError( diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/common/timeouts/timeout.py b/localstack-core/localstack/services/stepfunctions/asl/component/common/timeouts/timeout.py index 980fdf27854ee..03ae1a6ba2e33 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/common/timeouts/timeout.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/common/timeouts/timeout.py @@ -1,12 +1,28 @@ import abc from typing import Final, Optional +from localstack.aws.api.stepfunctions import ( + ExecutionFailedEventDetails, + HistoryEventType, +) +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) from localstack.services.stepfunctions.asl.component.common.string.string_expression import ( StringJSONata, StringSampler, ) from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.asl.utils.json_path import NoSuchJsonPathError class EvalTimeoutError(TimeoutError): @@ -72,7 +88,23 @@ def is_default_value(self) -> bool: return False def _eval_seconds(self, env: Environment) -> int: - self.string_sampler.eval(env=env) + try: + self.string_sampler.eval(env=env) + except NoSuchJsonPathError as no_such_json_path_error: + json_path = no_such_json_path_error.json_path + cause = f"Invalid path '{json_path}' : No results for path: $['{json_path[2:]}']" + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.ExecutionFailed, + event_details=EventDetails( + executionFailedEventDetails=ExecutionFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), + ) + ) seconds = env.stack.pop() if not isinstance(seconds, int) and seconds <= 0: raise ValueError( diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py index 37964efa9cf9a..5880257203512 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state.py @@ -13,6 +13,7 @@ HistoryEventType, StateEnteredEventDetails, StateExitedEventDetails, + TaskFailedEventDetails, ) from localstack.services.stepfunctions.asl.component.common.assign.assign_decl import AssignDecl from localstack.services.stepfunctions.asl.component.common.catch.catch_outcome import CatchOutcome @@ -55,6 +56,7 @@ from localstack.services.stepfunctions.asl.eval.program_state import ProgramRunning from localstack.services.stepfunctions.asl.eval.states import StateData from localstack.services.stepfunctions.asl.utils.encoding import to_json_str +from localstack.services.stepfunctions.asl.utils.json_path import NoSuchJsonPathError from localstack.services.stepfunctions.quotas import is_within_size_quota LOG = logging.getLogger(__name__) @@ -208,7 +210,26 @@ def _eval_body(self, env: Environment) -> None: env.stack.append(env.states.get_input()) # Exec the state's logic. - self._eval_state(env) + try: + self._eval_state(env) + except NoSuchJsonPathError as no_such_json_path_error: + data_json_str = to_json_str(no_such_json_path_error.data) + cause = ( + f"The JSONPath '{no_such_json_path_error.json_path}' specified for the field '{env.next_field_name}' " + f"could not be found in the input '{data_json_str}'" + ) + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), + ) + ) if not isinstance(env.program_state(), ProgramRunning): return diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_wait/wait_function/seconds_path.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_wait/wait_function/seconds_path.py index cd13f59b281bd..af840602c5133 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_wait/wait_function/seconds_path.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_wait/wait_function/seconds_path.py @@ -1,6 +1,9 @@ from typing import Any, Final -from localstack.aws.api.stepfunctions import ExecutionFailedEventDetails, HistoryEventType +from localstack.aws.api.stepfunctions import ( + ExecutionFailedEventDetails, + HistoryEventType, +) from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( FailureEvent, FailureEventException, @@ -19,6 +22,7 @@ ) from localstack.services.stepfunctions.asl.eval.environment import Environment from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.asl.utils.json_path import NoSuchJsonPathError class SecondsPath(WaitFunction): @@ -58,7 +62,22 @@ def _validate_seconds_value(self, env: Environment, seconds: Any): ) def _get_wait_seconds(self, env: Environment) -> int: - self.string_sampler.eval(env=env) + try: + self.string_sampler.eval(env=env) + except NoSuchJsonPathError as no_such_json_path_error: + cause = f"The SecondsPath parameter does not reference an input value: {no_such_json_path_error.json_path}" + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.ExecutionFailed, + event_details=EventDetails( + executionFailedEventDetails=ExecutionFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=cause + ) + ), + ) + ) seconds = env.stack.pop() self._validate_seconds_value(env=env, seconds=seconds) return seconds diff --git a/tests/aws/services/stepfunctions/v2/logs/test_logs.py b/tests/aws/services/stepfunctions/v2/logs/test_logs.py index 3eb7a32dec710..098b220010361 100644 --- a/tests/aws/services/stepfunctions/v2/logs/test_logs.py +++ b/tests/aws/services/stepfunctions/v2/logs/test_logs.py @@ -69,7 +69,6 @@ class TestLogs: _TEST_BASE_CONFIGURATIONS, ids=_TEST_BASE_CONFIGURATIONS_IDS, ) - @markers.snapshot.skip_snapshot_verify(paths=["$..cause"]) def test_base( self, aws_client, @@ -103,7 +102,6 @@ def test_base( _TEST_PARTIAL_LOG_LEVEL_CONFIGURATIONS, ids=_TEST_PARTIAL_LOG_LEVEL_CONFIGURATIONS_IDS, ) - @markers.snapshot.skip_snapshot_verify(paths=["$..cause"]) def test_partial_log_levels( self, aws_client, diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py index b8626cb2986f6..ac7732f1150b3 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py @@ -2505,8 +2505,20 @@ def test_fail_cause_jsonata( ST.load_sfn_template(ST.INVALID_JSONPATH_IN_CAUSEPATH), ST.load_sfn_template(ST.INVALID_JSONPATH_IN_INPUTPATH), ST.load_sfn_template(ST.INVALID_JSONPATH_IN_OUTPUTPATH), - ST.load_sfn_template(ST.INVALID_JSONPATH_IN_TIMEOUTSECONDSPATH), - ST.load_sfn_template(ST.INVALID_JSONPATH_IN_HEARTBEATSECONDSPATH), + pytest.param( + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_TIMEOUTSECONDSPATH), + marks=pytest.mark.skipif( + condition=not is_aws_cloud(), + reason="timeout computation is run at the state's level", + ), + ), + pytest.param( + ST.load_sfn_template(ST.INVALID_JSONPATH_IN_HEARTBEATSECONDSPATH), + marks=pytest.mark.skipif( + condition=not is_aws_cloud(), + reason="heartbeat computation is run at the state's level", + ), + ), ], ids=[ "INVALID_JSONPATH_IN_ERRORPATH", From db33c9d7811fd53ee3a83937ccb48d6f262dfe32 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:12:30 +0100 Subject: [PATCH 4/4] fix legacy jsonata test --- tests/unit/services/stepfunctions/test_jsonata_payload.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/services/stepfunctions/test_jsonata_payload.py b/tests/unit/services/stepfunctions/test_jsonata_payload.py index a7da6f708b9e1..f1ed278830a01 100644 --- a/tests/unit/services/stepfunctions/test_jsonata_payload.py +++ b/tests/unit/services/stepfunctions/test_jsonata_payload.py @@ -105,6 +105,7 @@ def evaluate_payload(jsonata_payload_object: AssignTemplateValueObject) -> dict: activity_store=dict(), ) env._program_state = ProgramRunning() + env.next_state_name = "test-state" jsonata_payload_object.eval(env) return env.stack.pop()