diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03e29508fe..d9af2f5e4a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -210,6 +210,7 @@ jobs: with: # These are the versions of Python that correspond to the supported Lambda runtimes python-version: | + 3.11 3.10 3.9 3.8 diff --git a/Makefile b/Makefile index 027b9adab1..c32402119e 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,9 @@ SAM_CLI_TELEMETRY ?= 0 init: + pip install wheel + pip install "cython<3.0.0" pyyaml==5.4.1 --no-build-isolation + pip uninstall wheel cython --yes SAM_CLI_DEV=1 pip install -e '.[dev]' test: diff --git a/installer/pyinstaller/build-linux.sh b/installer/pyinstaller/build-linux.sh index 2b728866da..0da95c0814 100755 --- a/installer/pyinstaller/build-linux.sh +++ b/installer/pyinstaller/build-linux.sh @@ -76,6 +76,16 @@ cd .. echo "Installing Python Libraries" python3 -m venv venv ./venv/bin/pip install --upgrade pip + +# https://github.com/yaml/pyyaml/issues/724 +echo "Force cython package version to be lower than 3.x.x" +./venv/bin/pip install wheel +./venv/bin/pip install --no-build-isolation "cython<3.0.0" pyyaml==5.4.1 + +echo "Uninstall local cython package" +./venv/bin/pip uninstall wheel cython --yes +# end cython work around + ./venv/bin/pip install -r src/requirements/reproducible-linux.txt echo "Copying All Python Libraries" diff --git a/installer/pyinstaller/build-mac.sh b/installer/pyinstaller/build-mac.sh index 7a69a6b0a4..0ecdfaef3e 100644 --- a/installer/pyinstaller/build-mac.sh +++ b/installer/pyinstaller/build-mac.sh @@ -86,6 +86,16 @@ cd .. echo "Installing Python Libraries" /usr/local/bin/python3.8 -m venv venv ./venv/bin/pip install --upgrade pip + +# https://github.com/yaml/pyyaml/issues/724 +echo "Force cython package version to be lower than 3.x.x" +./venv/bin/pip install wheel +./venv/bin/pip install --no-build-isolation "cython<3.0.0" pyyaml==5.4.1 + +echo "Uninstall local cython package" +./venv/bin/pip uninstall wheel cython --yes +# end cython work around + ./venv/bin/pip install -r src/requirements/reproducible-mac.txt echo "Copying All Python Libraries" diff --git a/requirements/base.txt b/requirements/base.txt index 57a67e078b..8bbc0b0f2b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -15,7 +15,7 @@ docker~=6.1.0 dateparser~=1.1 requests~=2.31.0 serverlessrepo==0.1.10 -aws_lambda_builders==1.34.0 +aws_lambda_builders==1.35.0 tomlkit==0.11.8 watchdog==2.1.2 rich~=13.4.2 diff --git a/requirements/dev.txt b/requirements/dev.txt index 55f6a457c1..6acd7e677b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -24,7 +24,7 @@ types-requests==2.31.0.1 types-urllib3==1.26.25.13 # Test requirements -pytest~=7.2.2 +pytest~=7.4.0 parameterized==0.9.0 pytest-xdist==3.2.0 pytest-forked==1.6.0 diff --git a/requirements/pyinstaller-build.txt b/requirements/pyinstaller-build.txt index 400eaac4e8..3d1a3e564b 100644 --- a/requirements/pyinstaller-build.txt +++ b/requirements/pyinstaller-build.txt @@ -1,3 +1,3 @@ # Executable binary builder requirements setuptools==67.7.2 -pyinstaller==5.10.1 +pyinstaller==5.13.0 diff --git a/requirements/reproducible-linux.txt b/requirements/reproducible-linux.txt index 8e993c68de..44ddfa4122 100644 --- a/requirements/reproducible-linux.txt +++ b/requirements/reproducible-linux.txt @@ -15,9 +15,9 @@ attrs==23.1.0 \ # jschema-to-python # jsonschema # sarif-om -aws-lambda-builders==1.34.0 \ - --hash=sha256:0790f7e9b7ee7286b96fbcf49450c5be0341bb7cb852ca7d74beae190139eb48 \ - --hash=sha256:20456a942a417407b42ecf8ab7fce6a47306fd063051e7cb09d02d1be24d5cf3 +aws-lambda-builders==1.35.0 \ + --hash=sha256:419d766e60ac2a7303a23889b354d108a4244ce8d467dcf9dc71a62461895780 \ + --hash=sha256:cf462961ec8c9d493f82b955d6b76630dc0ed356b166caddd5436719ce599586 # via aws-sam-cli (setup.py) aws-sam-translator==1.71.0 \ --hash=sha256:17fb87c8137d8d49e7a978396b2b3b279211819dee44618415aab1e99c2cb659 \ diff --git a/requirements/reproducible-mac.txt b/requirements/reproducible-mac.txt index 7e7740fe0c..1308d90f05 100644 --- a/requirements/reproducible-mac.txt +++ b/requirements/reproducible-mac.txt @@ -15,9 +15,9 @@ attrs==23.1.0 \ # jschema-to-python # jsonschema # sarif-om -aws-lambda-builders==1.34.0 \ - --hash=sha256:0790f7e9b7ee7286b96fbcf49450c5be0341bb7cb852ca7d74beae190139eb48 \ - --hash=sha256:20456a942a417407b42ecf8ab7fce6a47306fd063051e7cb09d02d1be24d5cf3 +aws-lambda-builders==1.35.0 \ + --hash=sha256:419d766e60ac2a7303a23889b354d108a4244ce8d467dcf9dc71a62461895780 \ + --hash=sha256:cf462961ec8c9d493f82b955d6b76630dc0ed356b166caddd5436719ce599586 # via aws-sam-cli (setup.py) aws-sam-translator==1.71.0 \ --hash=sha256:17fb87c8137d8d49e7a978396b2b3b279211819dee44618415aab1e99c2cb659 \ @@ -271,9 +271,9 @@ idna==3.4 \ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 # via requests -importlib-metadata==6.7.0 \ - --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ - --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 +importlib-metadata==6.8.0 \ + --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ + --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 # via flask importlib-resources==5.12.0 \ --hash=sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6 \ diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index 86327d411d..80463d9d6b 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -51,7 +51,7 @@ \b Supported Runtimes ------------------ - 1. Python 3.7, 3.8, 3.9, 3.10 using PIP\n + 1. Python 3.7, 3.8, 3.9, 3.10, 3.11 using PIP\n 2. Nodejs 18.x, 16.x, 14.x, 12.x using NPM\n 3. Ruby 2.7, 3.2 using Bundler\n 4. Java 8, Java 11, Java 17 using Gradle and Maven\n diff --git a/samcli/lib/build/bundler.py b/samcli/lib/build/bundler.py index cd69604083..ba23158d39 100644 --- a/samcli/lib/build/bundler.py +++ b/samcli/lib/build/bundler.py @@ -174,7 +174,8 @@ def _get_path_and_filename_from_handler(handler: str) -> Optional[str]: :return: string path to built handler file """ try: - path = str(Path(handler).parent / Path(handler).stem) + ".js" + path = (Path(handler).parent / Path(handler).stem).as_posix() + path = path + ".js" except (AttributeError, TypeError): return None return path diff --git a/samcli/lib/build/workflow_config.py b/samcli/lib/build/workflow_config.py index b9ac751aed..713c57a751 100644 --- a/samcli/lib/build/workflow_config.py +++ b/samcli/lib/build/workflow_config.py @@ -89,6 +89,7 @@ def get_layer_subfolder(build_workflow: str) -> str: "python3.8": "python", "python3.9": "python", "python3.10": "python", + "python3.11": "python", "nodejs4.3": "nodejs", "nodejs6.10": "nodejs", "nodejs8.10": "nodejs", @@ -155,6 +156,7 @@ def get_workflow_config( "python3.8": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "python3.9": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "python3.10": BasicWorkflowSelector(PYTHON_PIP_CONFIG), + "python3.11": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "nodejs12.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs14.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs16.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), diff --git a/samcli/lib/providers/cfn_api_provider.py b/samcli/lib/providers/cfn_api_provider.py index 93e2915276..ca2531362a 100644 --- a/samcli/lib/providers/cfn_api_provider.py +++ b/samcli/lib/providers/cfn_api_provider.py @@ -522,7 +522,7 @@ def _extract_cfn_gateway_v2_stage( api_resource_type = resources.get(api_id, {}).get("Type") if api_resource_type != AWS_APIGATEWAY_V2_API: raise InvalidSamTemplateException( - "The AWS::ApiGatewayV2::Stag must have a valid ApiId that points to Api resource {}".format(api_id) + "The AWS::ApiGatewayV2::Stage must have a valid ApiId that points to Api resource {}".format(api_id) ) collector.stage_name = stage_name diff --git a/samcli/lib/remote_invoke/lambda_invoke_executors.py b/samcli/lib/remote_invoke/lambda_invoke_executors.py index 911bdb0a96..507999210e 100644 --- a/samcli/lib/remote_invoke/lambda_invoke_executors.py +++ b/samcli/lib/remote_invoke/lambda_invoke_executors.py @@ -159,16 +159,16 @@ class DefaultConvertToJSON(RemoteInvokeRequestResponseMapper[RemoteInvokeExecuti def map(self, test_input: RemoteInvokeExecutionInfo) -> RemoteInvokeExecutionInfo: if not test_input.is_file_provided(): if not test_input.payload: - LOG.debug("Input event not found, invoking Lambda Function with an empty event") + LOG.debug("Input event not found, invoking resource with an empty event") test_input.payload = "{}" - LOG.debug("Mapping input Payload to JSON string object") + LOG.debug("Mapping input event to JSON string object") try: _ = json.loads(cast(str, test_input.payload)) except JSONDecodeError: json_value = json.dumps(test_input.payload) LOG.info( "Auto converting value '%s' into JSON '%s'. " - "If you don't want auto-conversion, please provide a JSON string as payload", + "If you don't want auto-conversion, please provide a JSON string as event", test_input.payload, json_value, ) diff --git a/samcli/lib/utils/architecture.py b/samcli/lib/utils/architecture.py index a8025d8b3f..0121d5d8f0 100644 --- a/samcli/lib/utils/architecture.py +++ b/samcli/lib/utils/architecture.py @@ -22,6 +22,7 @@ "python3.8": [ARM64, X86_64], "python3.9": [ARM64, X86_64], "python3.10": [ARM64, X86_64], + "python3.11": [ARM64, X86_64], "ruby2.7": [ARM64, X86_64], "ruby3.2": [ARM64, X86_64], "java8": [X86_64], diff --git a/samcli/lib/utils/preview_runtimes.py b/samcli/lib/utils/preview_runtimes.py index c17ae95cf8..cffd6b2338 100644 --- a/samcli/lib/utils/preview_runtimes.py +++ b/samcli/lib/utils/preview_runtimes.py @@ -4,4 +4,4 @@ """ from typing import Set -PREVIEW_RUNTIMES: Set[str] = set() +PREVIEW_RUNTIMES: Set[str] = {"python3.11"} diff --git a/samcli/local/common/runtime_template.py b/samcli/local/common/runtime_template.py index 697631adea..707ea0f3f3 100644 --- a/samcli/local/common/runtime_template.py +++ b/samcli/local/common/runtime_template.py @@ -16,7 +16,7 @@ RUNTIME_DEP_TEMPLATE_MAPPING = { "python": [ { - "runtimes": ["python3.10", "python3.9", "python3.8", "python3.7"], + "runtimes": ["python3.11", "python3.10", "python3.9", "python3.8", "python3.7"], "dependency_manager": "pip", "init_location": os.path.join(_templates, "cookiecutter-aws-sam-hello-python"), "build": True, @@ -113,6 +113,7 @@ def get_local_lambda_images_location(mapping, runtime): "provided.al2", "provided", # python runtimes in descending order + "python3.11", "python3.10", "python3.9", "python3.8", @@ -135,6 +136,7 @@ def get_local_lambda_images_location(mapping, runtime): "nodejs16.x": "amazon/nodejs16.x-base", "nodejs14.x": "amazon/nodejs14.x-base", "nodejs12.x": "amazon/nodejs12.x-base", + "python3.11": "amazon/python3.11-base", "python3.10": "amazon/python3.10-base", "python3.9": "amazon/python3.9-base", "python3.8": "amazon/python3.8-base", @@ -156,6 +158,7 @@ def get_local_lambda_images_location(mapping, runtime): "python3.8": "Python36", "python3.9": "Python36", "python3.10": "Python36", + "python3.11": "Python36", "dotnet6": "dotnet6", "go1.x": "Go1", } diff --git a/samcli/local/docker/lambda_debug_settings.py b/samcli/local/docker/lambda_debug_settings.py index 8bffc6a3e2..a5e378dea1 100644 --- a/samcli/local/docker/lambda_debug_settings.py +++ b/samcli/local/docker/lambda_debug_settings.py @@ -179,6 +179,10 @@ def get_debug_settings(debug_port, debug_args_list, _container_env_vars, runtime entry + ["/var/lang/bin/python3.10"] + debug_args_list + ["/var/runtime/bootstrap.py"], container_env_vars=_container_env_vars, ), + Runtime.python311.value: lambda: DebugSettings( + entry + ["/var/lang/bin/python3.11"] + debug_args_list + ["/var/runtime/bootstrap.py"], + container_env_vars=_container_env_vars, + ), } try: return entrypoint_mapping[runtime]() diff --git a/samcli/local/docker/lambda_image.py b/samcli/local/docker/lambda_image.py index 23f0a770d9..923b740edc 100644 --- a/samcli/local/docker/lambda_image.py +++ b/samcli/local/docker/lambda_image.py @@ -39,6 +39,7 @@ class Runtime(Enum): python38 = "python3.8" python39 = "python3.9" python310 = "python3.10" + python311 = "python3.11" ruby27 = "ruby2.7" ruby32 = "ruby3.2" java8 = "java8" diff --git a/samcli/runtime_config.json b/samcli/runtime_config.json index 3609a8eea6..de059089e9 100644 --- a/samcli/runtime_config.json +++ b/samcli/runtime_config.json @@ -1,3 +1,3 @@ { - "app_template_repo_commit": "bb905c379830c3d8edbc196bda731076549028e3" + "app_template_repo_commit": "70788081366ff232a25a8b31961f59d27103e449" } diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index b45d2a2418..6e7787381e 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -440,6 +440,7 @@ def _validate_skipped_built_function( ("template.yaml", "Function", True, "python3.8", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.9", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.10", "Python", False, "CodeUri"), + ("template.yaml", "Function", True, "python3.11", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.7", "PythonPEP600", False, "CodeUri"), ("template.yaml", "Function", True, "python3.8", "PythonPEP600", False, "CodeUri"), ], @@ -479,6 +480,7 @@ def test_with_default_requirements(self): ("template.yaml", "Function", True, "python3.8", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.9", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.10", "Python", False, "CodeUri"), + ("template.yaml", "Function", True, "python3.11", "Python", False, "CodeUri"), ], ) class TestBuildCommand_PythonFunctions_WithDocker(BuildIntegPythonBase): diff --git a/tests/integration/remote/invoke/remote_invoke_integ_base.py b/tests/integration/remote/invoke/remote_invoke_integ_base.py index b0bd0cdaf8..4cf9d01a18 100644 --- a/tests/integration/remote/invoke/remote_invoke_integ_base.py +++ b/tests/integration/remote/invoke/remote_invoke_integ_base.py @@ -7,6 +7,7 @@ run_command, ) from tests.integration.deploy.deploy_integ_base import DeployIntegBase +from samcli.lib.remote_invoke.remote_invoke_executor_factory import RemoteInvokeExecutorFactory from samcli.lib.utils.boto_utils import get_boto_resource_provider_with_config, get_boto_client_provider_with_config from samcli.lib.utils.cloudformation import get_resource_summaries @@ -47,17 +48,17 @@ def remote_invoke_deploy_stack(stack_name, template_path): @classmethod def create_resources_and_boto_clients(cls): cls.remote_invoke_deploy_stack(cls.stack_name, cls.template_path) - stack_resource_summaries = get_resource_summaries( + boto_client_provider = get_boto_client_provider_with_config() + cls.stack_resource_summaries = get_resource_summaries( get_boto_resource_provider_with_config(), - get_boto_client_provider_with_config(), + boto_client_provider, cls.stack_name, ) - cls.stack_resources = { - resource_full_path: stack_resource_summary.physical_resource_id - for resource_full_path, stack_resource_summary in stack_resource_summaries.items() - } - cls.cfn_client = get_boto_client_provider_with_config()("cloudformation") - cls.lambda_client = get_boto_client_provider_with_config()("lambda") + cls.supported_resources = RemoteInvokeExecutorFactory.REMOTE_INVOKE_EXECUTOR_MAPPING.keys() + cls.cfn_client = boto_client_provider("cloudformation") + cls.lambda_client = boto_client_provider("lambda") + cls.stepfunctions_client = boto_client_provider("stepfunctions") + cls.xray_client = boto_client_provider("xray") @staticmethod def get_command_list( diff --git a/tests/integration/remote/invoke/test_lambda_invoke_response_stream.py b/tests/integration/remote/invoke/test_lambda_invoke_response_stream.py index 5adf7bdba5..ae07fc0a97 100644 --- a/tests/integration/remote/invoke/test_lambda_invoke_response_stream.py +++ b/tests/integration/remote/invoke/test_lambda_invoke_response_stream.py @@ -15,7 +15,7 @@ class TestInvokeResponseStreamingLambdas(RemoteInvokeIntegBase): @classmethod def setUpClass(cls): super().setUpClass() - cls.stack_name = f"{TestInvokeResponseStreamingLambdas.__name__}-{uuid.uuid4().hex}" + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" cls.create_resources_and_boto_clients() def test_invoke_empty_event_provided(self): diff --git a/tests/integration/remote/invoke/test_remote_invoke.py b/tests/integration/remote/invoke/test_remote_invoke.py index e3eb6aa2de..7001af4ec2 100644 --- a/tests/integration/remote/invoke/test_remote_invoke.py +++ b/tests/integration/remote/invoke/test_remote_invoke.py @@ -1,8 +1,10 @@ import json import uuid import base64 +import time from parameterized import parameterized +from unittest import skip from tests.integration.remote.invoke.remote_invoke_integ_base import RemoteInvokeIntegBase from tests.testing_utils import run_command @@ -12,13 +14,13 @@ @pytest.mark.xdist_group(name="sam_remote_invoke_single_lambda_resource") -class TestSingleResourceInvoke(RemoteInvokeIntegBase): +class TestSingleLambdaInvoke(RemoteInvokeIntegBase): template = Path("template-single-lambda.yaml") @classmethod def setUpClass(cls): super().setUpClass() - cls.stack_name = f"{TestSingleResourceInvoke.__name__}-{uuid.uuid4().hex}" + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" cls.create_resources_and_boto_clients() def test_invoke_empty_event_provided(self): @@ -29,7 +31,7 @@ def test_invoke_empty_event_provided(self): remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) self.assertEqual(remote_invoke_result_stdout["errorType"], "KeyError") - def test_invoke_with_only_event_provided(self): + def test_invoke_with_event_provided(self): command_list = self.get_command_list( stack_name=self.stack_name, event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', @@ -41,7 +43,7 @@ def test_invoke_with_only_event_provided(self): remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) - def test_invoke_with_only_event_file_provided(self): + def test_invoke_with_event_file_provided(self): event_file_path = str(self.events_folder_path.joinpath("default_event.json")) command_list = self.get_command_list( stack_name=self.stack_name, resource_id="HelloWorldFunction", event_file=event_file_path @@ -55,7 +57,7 @@ def test_invoke_with_only_event_file_provided(self): def test_invoke_with_resource_id_provided_as_arn(self): resource_id = "HelloWorldFunction" - lambda_name = self.stack_resources[resource_id] + lambda_name = self.stack_resource_summaries[resource_id].physical_resource_id lambda_arn = self.lambda_client.get_function(FunctionName=lambda_name)["Configuration"]["FunctionArn"] command_list = self.get_command_list( @@ -116,23 +118,88 @@ def test_invoke_response_json_output_format(self): self.assertEqual(remote_invoke_result_stdout["StatusCode"], 200) +@skip("Skip remote invoke Step function integration tests") +@pytest.mark.xdist_group(name="sam_remote_invoke_sfn_resource_priority") +class TestSFNPriorityInvoke(RemoteInvokeIntegBase): + template = Path("template-step-function-priority.yaml") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" + cls.create_resources_and_boto_clients() + + def test_invoke_empty_event_provided(self): + command_list = self.get_command_list(stack_name=self.stack_name) + expected_response = "Hello World" + + remote_invoke_result = run_command(command_list) + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + @parameterized.expand( + [('{"is_developer": false}', "Hello World"), ('{"is_developer": true}', "Hello Developer World")] + ) + def test_invoke_with_event_provided(self, event, expected_response): + command_list = self.get_command_list( + stack_name=self.stack_name, + event=event, + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_invoke_with_event_file_provided(self): + event_file_path = str(self.events_folder_path.joinpath("sfn_input_event.json")) + expected_response = "Hello Developer World" + command_list = self.get_command_list(stack_name=self.stack_name, event_file=event_file_path) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_invoke_boto_parameters(self): + expected_response = "Hello World" + command_list = self.get_command_list( + stack_name=self.stack_name, + event='{"is_developer": false}', + parameter_list=[("name", "custom-execution-name"), ("traceHeader", "Root=not enabled;Sampled=0")], + output="json", + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(json.loads(remote_invoke_result_stdout["output"]), expected_response) + + # Overriding traceHeader with not enabled will return a dummy value + dummy_trace_id_returned = remote_invoke_result_stdout["traceHeader"][5:40] + time.sleep(3) + + get_xrays_response = self.xray_client.batch_get_traces(TraceIds=[dummy_trace_id_returned]) + self.assertEqual([], get_xrays_response["Traces"]) + + @pytest.mark.xdist_group(name="sam_remote_invoke_multiple_resources") class TestMultipleResourcesInvoke(RemoteInvokeIntegBase): template = Path("template-multiple-resources.yaml") - @classmethod - def tearDownClass(cls): - # Delete the deployed stack - cls.cfn_client.delete_stack(StackName=cls.stack_name) - @classmethod def setUpClass(cls): super().setUpClass() - cls.stack_name = f"{TestMultipleResourcesInvoke.__name__}-{uuid.uuid4().hex}" + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" cls.create_resources_and_boto_clients() def test_invoke_empty_event_provided(self): - command_list = self.get_command_list(stack_name=self.stack_name, resource_id="EchoEventFunction") + resource_id = "EchoEventFunction" + command_list = self.get_command_list(stack_name=self.stack_name, resource_id=resource_id) remote_invoke_result = run_command(command_list) self.assertEqual(0, remote_invoke_result.process.returncode) @@ -141,16 +208,29 @@ def test_invoke_empty_event_provided(self): @parameterized.expand( [ - ("HelloWorldServerlessFunction", {"message": "Hello world"}), - ("EchoCustomEnvVarFunction", "MyOtherVar"), - ("EchoEventFunction", {"key1": "Hello", "key2": "serverless", "key3": "world"}), + ( + "HelloWorldServerlessFunction", + '{"key1": "Hello", "key2": "serverless", "key3": "world"}', + {"message": "Hello world"}, + ), + ("EchoCustomEnvVarFunction", '{"key1": "Hello", "key2": "serverless", "key3": "world"}', "MyOtherVar"), + ( + "EchoEventFunction", + '{"key1": "Hello", "key2": "serverless", "key3": "world"}', + {"key1": "Hello", "key2": "serverless", "key3": "world"}, + ), + ("StockPriceGuideStateMachine", '{"stock_price": 60, "balance": 200, "qty": 2}', {"balance": 320}), + ("StockPriceGuideStateMachine", '{"stock_price": 30, "balance": 200, "qty": 2}', {"balance": 140}), ] ) - def test_invoke_with_only_event_provided(self, resource_id, expected_response): + def test_invoke_with_only_event_provided(self, resource_id, event, expected_response): + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + command_list = self.get_command_list( stack_name=self.stack_name, resource_id=resource_id, - event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + event=event, ) remote_invoke_result = run_command(command_list) @@ -166,8 +246,8 @@ def test_invoke_with_only_event_provided(self, resource_id, expected_response): ("EchoEventFunction", {"key1": "Hello", "key2": "serverless", "key3": "world"}), ] ) - def test_invoke_with_resource_id_provided_as_arn(self, resource_id, expected_response): - lambda_name = self.stack_resources[resource_id] + def test_lambda_invoke_with_resource_id_provided_as_arn(self, resource_id, expected_response): + lambda_name = self.stack_resource_summaries[resource_id].physical_resource_id lambda_arn = self.lambda_client.get_function(FunctionName=lambda_name)["Configuration"]["FunctionArn"] command_list = self.get_command_list( @@ -182,7 +262,7 @@ def test_invoke_with_resource_id_provided_as_arn(self, resource_id, expected_res self.assertEqual(remote_invoke_result_stdout, expected_response) def test_lambda_writes_to_stderr_invoke(self): - command_list = RemoteInvokeIntegBase.get_command_list( + command_list = self.get_command_list( stack_name=self.stack_name, resource_id="WriteToStderrFunction", event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', @@ -229,48 +309,112 @@ def test_lambda_invoke_client_context_boto_parameter(self): remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) self.assertEqual(remote_invoke_result_stdout, custom_json_str["custom"]) + def test_sfn_invoke_with_resource_id_provided_as_arn(self): + resource_id = "StockPriceGuideStateMachine" + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + expected_response = {"balance": 320} + state_machine_arn = self.stack_resource_summaries[resource_id].physical_resource_id + + command_list = self.get_command_list( + resource_id=state_machine_arn, + event='{"stock_price": 60, "balance": 200, "qty": 2}', + ) + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_sfn_invoke_boto_parameters(self): + resource_id = "StockPriceGuideStateMachine" + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + expected_response = {"balance": 320} + name = "custom-execution-name" + command_list = self.get_command_list( + stack_name=self.stack_name, + resource_id=resource_id, + event='{"stock_price": 60, "balance": 200, "qty": 2}', + parameter_list=[("name", name)], + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_sfn_invoke_execution_fails(self): + resource_id = "StateMachineExecutionFails" + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + expected_response = "The execution failed due to the error: MockError and cause: Mock Invalid response." + command_list = self.get_command_list( + stack_name=self.stack_name, + resource_id=resource_id, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + remote_invoke_stderr = remote_invoke_result.stderr.strip().decode() + + self.assertEqual(0, remote_invoke_result.process.returncode) + self.assertIn(expected_response, remote_invoke_stderr) + @pytest.mark.xdist_group(name="sam_remote_invoke_nested_resources") class TestNestedTemplateResourcesInvoke(RemoteInvokeIntegBase): template = Path("nested_templates/template.yaml") - @classmethod - def tearDownClass(cls): - # Delete the deployed stack - cls.cfn_client.delete_stack(StackName=cls.stack_name) - @classmethod def setUpClass(cls): super().setUpClass() - cls.stack_name = f"{TestNestedTemplateResourcesInvoke.__name__}-{uuid.uuid4().hex}" + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" cls.create_resources_and_boto_clients() - def test_invoke_empty_event_provided(self): - command_list = self.get_command_list( - stack_name=self.stack_name, - ) + @parameterized.expand( + [ + ("ChildStack/HelloWorldFunction", {"message": "Hello world"}), + ("ChildStack/HelloWorldStateMachine", "World"), + ] + ) + def test_invoke_empty_event_provided(self, resource_id, expected_response): + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + command_list = self.get_command_list(stack_name=self.stack_name, resource_id=resource_id) remote_invoke_result = run_command(command_list) self.assertEqual(0, remote_invoke_result.process.returncode) remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) - self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + self.assertEqual(remote_invoke_result_stdout, expected_response) - def test_invoke_with_only_event_provided(self): + @parameterized.expand( + [ + ("ChildStack/HelloWorldFunction", '{"key1": "Hello", "key2": "world"}', {"message": "Hello world"}), + ("ChildStack/HelloWorldStateMachine", '{"key1": "Hello", "key2": "world"}', "World"), + ] + ) + def test_invoke_with_event_provided(self, resource_id, event, expected_response): + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") command_list = self.get_command_list( stack_name=self.stack_name, - resource_id="ChildStack/HelloWorldFunction", - event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + resource_id=resource_id, + event=event, ) remote_invoke_result = run_command(command_list) self.assertEqual(0, remote_invoke_result.process.returncode) remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) - self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + self.assertEqual(remote_invoke_result_stdout, expected_response) - def test_invoke_default_lambda_function(self): + def test_invoke_event_file_provided(self): event_file_path = str(self.events_folder_path.joinpath("default_event.json")) - command_list = self.get_command_list(stack_name=self.stack_name, event_file=event_file_path) + command_list = self.get_command_list( + stack_name=self.stack_name, resource_id="ChildStack/HelloWorldFunction", event_file=event_file_path + ) remote_invoke_result = run_command(command_list) diff --git a/tests/integration/testdata/remote_invoke/events/sfn_input_event.json b/tests/integration/testdata/remote_invoke/events/sfn_input_event.json new file mode 100644 index 0000000000..f9b87ca27e --- /dev/null +++ b/tests/integration/testdata/remote_invoke/events/sfn_input_event.json @@ -0,0 +1,3 @@ +{ + "is_developer": true +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/lambda-fns/main.py b/tests/integration/testdata/remote_invoke/lambda-fns/main.py index b3424d1b70..5770775adf 100644 --- a/tests/integration/testdata/remote_invoke/lambda-fns/main.py +++ b/tests/integration/testdata/remote_invoke/lambda-fns/main.py @@ -28,4 +28,28 @@ def echo_event(event, context): return event def raise_exception(event, context): - raise Exception("Lambda is raising an exception") \ No newline at end of file + raise Exception("Lambda is raising an exception") + +def stock_transaction_recommender(event, context): + stock_price = int(event["stock_price"]) + balance = event["balance"] + qty = event["qty"] + if qty*stock_price < 100: + stock_action = "Buy" + else: + stock_action = "Sell" + return {"stock_price": stock_price, "action": stock_action, "balance": balance, "qty": qty} + +def stock_buyer(event, context): + current_balance = event["balance"] + new_balance = current_balance - (event["qty"]*event["stock_price"]) + return { + "balance": new_balance + } + +def stock_seller(event, context): + current_balance = event["balance"] + new_balance = current_balance + (event["qty"]*event["stock_price"]) + return { + "balance": new_balance + } \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py b/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py index cce4a03dca..a6856732b2 100644 --- a/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py +++ b/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py @@ -1,4 +1,4 @@ def handler(event, context): return { "message": "Hello world", - } \ No newline at end of file + } diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/state-machines/hello-world-state-machine-definition.asl.json b/tests/integration/testdata/remote_invoke/nested_templates/childstack/state-machines/hello-world-state-machine-definition.asl.json new file mode 100644 index 0000000000..d2b8ad8e1a --- /dev/null +++ b/tests/integration/testdata/remote_invoke/nested_templates/childstack/state-machines/hello-world-state-machine-definition.asl.json @@ -0,0 +1,16 @@ +{ + "Comment": "A Hello World example of the Amazon States Language using Pass states", + "StartAt": "Hello", + "States": { + "Hello": { + "Type": "Pass", + "Result": "Hello", + "Next": "World" + }, + "World": { + "Type": "Pass", + "Result": "World", + "End": true + } + } +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml b/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml index c082eb0fe4..0f78d5a2a2 100644 --- a/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml +++ b/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml @@ -8,4 +8,9 @@ Resources: Handler: app.handler Runtime: python3.9 CodeUri: function/ - Timeout: 30 \ No newline at end of file + Timeout: 30 + + HelloWorldStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + DefinitionUri: ./state-machines/hello-world-state-machine-definition.asl.json \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/state-machines/execution-fails-state-machine-definition.asl.json b/tests/integration/testdata/remote_invoke/state-machines/execution-fails-state-machine-definition.asl.json new file mode 100644 index 0000000000..319d062841 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/state-machines/execution-fails-state-machine-definition.asl.json @@ -0,0 +1,16 @@ +{ + "Comment": "A Simple example of the Amazon States Language using Pass and Fail states", + "StartAt": "Hello", + "States": { + "Hello": { + "Type": "Pass", + "Result": "Hello", + "Next": "World" + }, + "World": { + "Type": "Fail", + "Cause": "Mock Invalid response.", + "Error": "MockError" + } + } + } \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/state-machines/hello-world-state-machine-definition.asl.json b/tests/integration/testdata/remote_invoke/state-machines/hello-world-state-machine-definition.asl.json new file mode 100644 index 0000000000..5cc4c63b8b --- /dev/null +++ b/tests/integration/testdata/remote_invoke/state-machines/hello-world-state-machine-definition.asl.json @@ -0,0 +1,32 @@ +{ + "Comment": "A Hello World example of the Amazon States Language using Pass states", + "StartAt": "Type of World", + "States": { + "Type of World": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.is_developer", + "IsPresent": false, + "Next": "World" + }, + { + "Variable": "$.is_developer", + "BooleanEquals": true, + "Next": "Developer World" + } + ], + "Default": "World" + }, + "World": { + "Type": "Pass", + "Result": "Hello World", + "End": true + }, + "Developer World": { + "Type": "Pass", + "Result": "Hello Developer World", + "End": true + } + } +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/state-machines/stock-trader-state-machine-definition.asl.json b/tests/integration/testdata/remote_invoke/state-machines/stock-trader-state-machine-definition.asl.json new file mode 100644 index 0000000000..5e23a065d1 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/state-machines/stock-trader-state-machine-definition.asl.json @@ -0,0 +1,62 @@ +{ + "Comment": "A state machine that does mock stock trading.", + "StartAt": "Recommend Stock Transaction", + "States": { + "Recommend Stock Transaction": { + "Type": "Task", + "Resource": "${StockActionRecommenderFunction}", + "Retry": [ + { + "ErrorEquals": [ + "States.TaskFailed" + ], + "IntervalSeconds": 14, + "MaxAttempts": 1, + "BackoffRate": 1.5 + } + ], + "Next": "Buy or Sell?" + }, + "Buy or Sell?": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.action", + "StringEquals": "Buy", + "Next": "Buy Stock" + } + ], + "Default": "Sell Stock" + }, + "Sell Stock": { + "Type": "Task", + "Resource": "${StockSellerFunctionArn}", + "Retry": [ + { + "ErrorEquals": [ + "States.TaskFailed" + ], + "IntervalSeconds": 2, + "MaxAttempts": 3, + "BackoffRate": 1 + } + ], + "End": true + }, + "Buy Stock": { + "Type": "Task", + "Resource": "${StockBuyerFunctionArn}", + "Retry": [ + { + "ErrorEquals": [ + "States.TaskFailed" + ], + "IntervalSeconds": 2, + "MaxAttempts": 3, + "BackoffRate": 1 + } + ], + "End": true + } + } +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml b/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml index ffebe530c1..a9e3f128a1 100644 --- a/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml +++ b/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml @@ -52,4 +52,46 @@ Resources: Handler: main.raise_exception Runtime: python3.9 CodeUri: ./lambda-fns - Timeout: 5 \ No newline at end of file + Timeout: 5 + + StateMachineExecutionFails: + Type: AWS::Serverless::StateMachine + Properties: + DefinitionUri: ./state-machines/execution-fails-state-machine-definition.asl.json + + StockPriceGuideStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + DefinitionUri: ./state-machines/stock-trader-state-machine-definition.asl.json + DefinitionSubstitutions: + StockActionRecommenderFunction: !GetAtt StockActionRecommenderFunction.Arn + StockSellerFunctionArn: !GetAtt StockSellerFunction.Arn + StockBuyerFunctionArn: !GetAtt StockBuyerFunction.Arn + Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html + - LambdaInvokePolicy: + FunctionName: !Ref StockActionRecommenderFunction + - LambdaInvokePolicy: + FunctionName: !Ref StockSellerFunction + - LambdaInvokePolicy: + FunctionName: !Ref StockBuyerFunction + + StockActionRecommenderFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html + Properties: + CodeUri: lambda-fns/ + Handler: main.stock_transaction_recommender + Runtime: python3.9 + + StockSellerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: lambda-fns/ + Handler: main.stock_seller + Runtime: python3.9 + + StockBuyerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: lambda-fns/ + Handler: main.stock_buyer + Runtime: python3.9 \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/template-step-function-priority.yaml b/tests/integration/testdata/remote_invoke/template-step-function-priority.yaml new file mode 100644 index 0000000000..e857d7d3d2 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/template-step-function-priority.yaml @@ -0,0 +1,11 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A hello world application with a step function. + +Resources: + HelloWorldStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + DefinitionUri: ./state-machines/hello-world-state-machine-definition.asl.json + Tracing: + Enabled: true \ No newline at end of file diff --git a/tests/unit/lib/build_module/test_bundler.py b/tests/unit/lib/build_module/test_bundler.py index c25543b09e..8bfc35daaf 100644 --- a/tests/unit/lib/build_module/test_bundler.py +++ b/tests/unit/lib/build_module/test_bundler.py @@ -1,10 +1,11 @@ from pathlib import Path -from unittest import TestCase +from unittest import TestCase, skipIf from unittest.mock import patch, Mock from parameterized import parameterized from samcli.lib.build.bundler import EsbuildBundlerManager +from tests.testing_utils import IS_WINDOWS from tests.unit.commands.buildcmd.test_build_context import DummyStack @@ -178,7 +179,7 @@ class PostProcessHandler(TestCase): def test_get_path_and_filename_from_handler(self): handler = "src/functions/FunctionName/app.Handler" file = EsbuildBundlerManager._get_path_and_filename_from_handler(handler) - expected_path = str(Path("src") / "functions" / "FunctionName" / "app.js") + expected_path = (Path("src") / "functions" / "FunctionName" / "app.js").as_posix() self.assertEqual(file, expected_path) @patch("samcli.lib.build.bundler.Path.__init__") @@ -240,3 +241,22 @@ def test_update_function_handler(self): self.assertEqual(updated_handler_a, "app.handler") self.assertEqual(updated_handler_b, "app.handler") self.assertEqual(updated_handler_c, "functions/source/update/app.handler") + + @parameterized.expand( + [("/opt/my/path/handler.handler", "/opt/my/path/handler.js"), ("handler.handler", "handler.js")] + ) + def test_get_handler_path_unix(self, input_path, expected_path): + result_path = EsbuildBundlerManager(Mock())._get_path_and_filename_from_handler(input_path) + + self.assertEqual(result_path, expected_path) + + @parameterized.expand( + [ + ("\\opt\\my\\path\\handler.handler", "/opt/my/path/handler.js"), + ] + ) + @skipIf(not IS_WINDOWS, "Skipping POSIX converting logic since WindowsPath is not available on unix systems") + def test_get_handler_windows_returns_posix(self, input_path, expected_path): + result_path = EsbuildBundlerManager(Mock())._get_path_and_filename_from_handler(input_path) + + self.assertEqual(result_path, expected_path) diff --git a/tests/unit/local/docker/test_lambda_container.py b/tests/unit/local/docker/test_lambda_container.py index 90893e4e49..bffa8bc1cc 100644 --- a/tests/unit/local/docker/test_lambda_container.py +++ b/tests/unit/local/docker/test_lambda_container.py @@ -23,6 +23,7 @@ Runtime.python38.value, Runtime.python39.value, Runtime.python310.value, + Runtime.python311.value, Runtime.dotnet6.value, ] diff --git a/tests/unit/local/docker/test_lambda_debug_settings.py b/tests/unit/local/docker/test_lambda_debug_settings.py index 1dd024ebc2..a72a9722a9 100644 --- a/tests/unit/local/docker/test_lambda_debug_settings.py +++ b/tests/unit/local/docker/test_lambda_debug_settings.py @@ -20,6 +20,7 @@ Runtime.python38, Runtime.python39, Runtime.python310, + Runtime.python311, ]