Skip to content

Commit d6ec326

Browse files
willyg302seshubaws
andauthored
feat: Support asynchronous shareable test events (#8373)
This feature was added recently to the Lambda console. It works by storing event metadata as an `x-metadata` OpenAPI extension on the schema's example. Here we support putting an event with a desired `--invocation-type` and passing that to a remote invoke (with an explicitly provided invocation type `--parameter` taking precedence). Co-authored-by: seshubaws <116689586+seshubaws@users.noreply.github.com>
1 parent 0c098f9 commit d6ec326

10 files changed

Lines changed: 250 additions & 23 deletions

File tree

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ init:
1313
fi
1414

1515
test:
16-
# Run unit tests
17-
# Fail if coverage falls below 95%
16+
# Run unit tests and fail if coverage falls below 94%
1817
pytest --cov samcli --cov schema --cov-report term-missing --cov-fail-under 94 tests/unit
1918

2019
test-cov-report:

samcli/commands/remote/invoke/cli.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,15 @@ def do_cli(
166166
and remote_invoke_context.resource_summary
167167
and remote_invoke_context.resource_summary.resource_type == AWS_LAMBDA_FUNCTION
168168
):
169-
lambda_test_event = remote_invoke_context.get_lambda_shared_test_event_provider()
170169
LOG.debug("Retrieving remote event %s", test_event_name)
171-
event = lambda_test_event.get_event(test_event_name, remote_invoke_context.resource_summary)
170+
lambda_test_event = remote_invoke_context.get_lambda_shared_test_event_provider().get_event(
171+
test_event_name, remote_invoke_context.resource_summary
172+
)
173+
event = lambda_test_event["json"]
172174
LOG.debug("Remote event contents: %s", event)
175+
metadata = lambda_test_event["metadata"]
176+
if "invocationType" in metadata:
177+
parameter.setdefault("InvocationType", metadata["invocationType"])
173178
elif test_event_name:
174179
LOG.info("Note: remote event is only supported for AWS Lambda Function resource.")
175180
test_event_name = ""

samcli/commands/remote/test_event/get/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def do_cli(
127127
raise UserException(str(ex), wrapped_from=ex.__class__.__name__) from ex
128128

129129
LOG.debug("Getting remote event '%s' for resource: %s", name, function_resource)
130-
event = lambda_test_event.get_event(name, function_resource)
130+
event = lambda_test_event.get_event(name, function_resource)["json"]
131131

132132
try:
133133
output_file.write(event)

samcli/commands/remote/test_event/put/cli.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from samcli.cli.cli_config_file import ConfigProvider, configuration_option
1212
from samcli.cli.context import Context
1313
from samcli.cli.main import aws_creds_options, common_options, pass_context, print_cmdline_args
14+
from samcli.commands._utils.click_mutex import ClickMutex
1415
from samcli.commands._utils.command_exception_handler import command_exception_handler
1516
from samcli.commands.remote.exceptions import IllFormedEventData
1617
from samcli.commands.remote.test_event.put.core.command import RemoteTestEventPutCommand
@@ -19,6 +20,7 @@
1920
stack_name_or_resource_id_atleast_one_option_validation,
2021
)
2122
from samcli.lib.telemetry.metric import track_command
23+
from samcli.lib.utils.invocation_type import EVENT, REQUEST_RESPONSE
2224
from samcli.lib.utils.version_checker import check_newer_version
2325
from samcli.yamlhelper import yaml_parse
2426

@@ -56,6 +58,14 @@
5658
"The option '--file' is required (You can provide '-' to read from stdin)"
5759
),
5860
)
61+
@click.option(
62+
"--invocation-type",
63+
type=click.Choice([EVENT, REQUEST_RESPONSE]),
64+
replace_help_option="--invocation-type INVOCATION_TYPE",
65+
help="Lambda function invocation type."
66+
+ click.style(f"\n\nInvocation types: {[EVENT, REQUEST_RESPONSE]}", bold=True),
67+
cls=ClickMutex,
68+
)
5969
@click.option("--force", "-f", is_flag=True, help="Force saving the event even if it already exists")
6070
@click.argument("resource-id", required=False)
6171
@aws_creds_options
@@ -72,6 +82,7 @@ def cli(
7282
resource_id: Optional[str],
7383
name: str,
7484
file: TextIOWrapper,
85+
invocation_type: str,
7586
force: bool,
7687
config_file: str,
7788
config_env: str,
@@ -84,6 +95,7 @@ def cli(
8495
resource_id,
8596
name,
8697
file,
98+
invocation_type,
8799
force,
88100
ctx.region,
89101
ctx.profile,
@@ -97,6 +109,7 @@ def do_cli(
97109
resource_id: Optional[str],
98110
name: str,
99111
file: TextIOWrapper,
112+
invocation_type: str,
100113
force: bool,
101114
region: str,
102115
profile: str,
@@ -147,7 +160,7 @@ def do_cli(
147160
data = yaml_parse(contents)
148161
event_data = json.dumps(data)
149162
LOG.debug("Creating remote event %s from resource: %s", name, function_resource)
150-
lambda_test_event.create_event(name, function_resource, event_data, force=force)
163+
lambda_test_event.create_event(name, function_resource, event_data, invocation_type, force=force)
151164
click.echo(f"Put remote event '{name}' completed successfully")
152165
except (ValueError, YAMLError, JSONDecodeError) as ex:
153166
raise IllFormedEventData(f"File {file.name} doesn't contain a valid JSON:\n {ex}") from ex

samcli/commands/remote/test_event/put/core/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# NOTE: The ordering of the option lists matter, they are the order
1919
# in which options will be displayed.
2020

21-
EVENT_OPTIONS: List[str] = ["name", "file", "force"]
21+
EVENT_OPTIONS: List[str] = ["name", "file", "invocation_type", "force"]
2222

2323
ALL_OPTIONS: List[str] = (
2424
INFRASTRUCTURE_OPTION_NAMES

samcli/lib/shared_test_events/lambda_shared_test_event.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import logging
55
from json import JSONDecodeError
6-
from typing import Any
6+
from typing import Any, Optional
77

88
import botocore
99

@@ -17,6 +17,7 @@
1717
)
1818
from samcli.lib.schemas.schemas_api_caller import SchemasApiCaller
1919
from samcli.lib.utils.cloudformation import CloudFormationResourceSummary
20+
from samcli.lib.utils.invocation_type import REQUEST_RESPONSE
2021
from samcli.lib.utils.resources import AWS_LAMBDA_FUNCTION
2122

2223
LAMBDA_TEST_EVENT_REGISTRY = "lambda-testevent-schemas"
@@ -142,7 +143,14 @@ def limit_versions(self, schema_name: str, registry_name: str = LAMBDA_TEST_EVEN
142143
)
143144
self._api_caller.delete_version(registry_name, schema_name, version_to_delete)
144145

145-
def add_event_to_schema(self, schema: str, event: str, event_name: str, replace_if_exists: bool = False) -> str:
146+
def add_event_to_schema(
147+
self,
148+
schema: str,
149+
event: str,
150+
event_name: str,
151+
metadata: Optional[dict[str, str]],
152+
replace_if_exists: bool = False,
153+
) -> str:
146154
"""
147155
Adds an event to a schema
148156
@@ -176,6 +184,8 @@ def add_event_to_schema(self, schema: str, event: str, event_name: str, replace_
176184
schema_dict["components"]["examples"][event_name] = {
177185
"value": event_dict,
178186
}
187+
if metadata:
188+
schema_dict["components"]["examples"][event_name]["x-metadata"] = metadata
179189

180190
schema = json.dumps(schema_dict)
181191

@@ -200,7 +210,7 @@ def get_event_from_schema(self, schema: str, event_name: str) -> Any:
200210
the name of the event to retrieve from the schema
201211
Returns
202212
-------
203-
The event data of the event "event_name"
213+
The event data and metadata of the event "event_name"
204214
"""
205215
try:
206216
schema_dict = json.loads(schema)
@@ -211,7 +221,7 @@ def get_event_from_schema(self, schema: str, event_name: str) -> Any:
211221
raise ResourceNotFound(f"Event {event_name} not found")
212222

213223
# can use square brackets here since we know each of these subdicts exist
214-
return existing_events[event_name]["value"]
224+
return existing_events[event_name]
215225

216226
except JSONDecodeError as ex:
217227
LOG.error("Error decoding schema when getting event %s", event_name)
@@ -263,6 +273,7 @@ def create_event(
263273
event_name: str,
264274
function_resource: CloudFormationResourceSummary,
265275
event_data: str,
276+
invocation_type: str = REQUEST_RESPONSE,
266277
force: bool = False,
267278
registry_name: str = LAMBDA_TEST_EVENT_REGISTRY,
268279
) -> str:
@@ -277,6 +288,8 @@ def create_event(
277288
The function where the event will be created
278289
event_data: str
279290
The JSON data of the event to be created
291+
invocation_type: str
292+
The Lambda invocation type of the event to be created
280293
registry_name: str
281294
The name of the registry that contains the schema
282295
"""
@@ -287,16 +300,19 @@ def create_event(
287300

288301
schema_name = self._get_schema_name(function_resource)
289302
original_schema = api_caller.get_schema(registry_name, schema_name)
303+
metadata = {"invocationType": invocation_type}
290304

291305
if original_schema:
292306
LOG.debug("Schema %s already exists, adding event %s", schema_name, event_name)
293307
self.limit_versions(schema_name, registry_name)
294-
schema = self.add_event_to_schema(original_schema, event_data, event_name, replace_if_exists=force)
308+
schema = self.add_event_to_schema(
309+
original_schema, event_data, event_name, metadata, replace_if_exists=force
310+
)
295311
api_caller.update_schema(schema, registry_name, schema_name, OPEN_API_TYPE)
296312
else:
297313
LOG.debug("Schema %s does not already exist, creating new", schema_name)
298314
schema = api_caller.discover_schema(event_data, OPEN_API_TYPE)
299-
schema = self.add_event_to_schema(schema, event_data, event_name)
315+
schema = self.add_event_to_schema(schema, event_data, event_name, metadata)
300316
api_caller.create_schema(schema, registry_name, schema_name, OPEN_API_TYPE)
301317

302318
return schema
@@ -346,7 +362,7 @@ def get_event(
346362
event_name: str,
347363
function_resource: CloudFormationResourceSummary,
348364
registry_name: str = LAMBDA_TEST_EVENT_REGISTRY,
349-
) -> str:
365+
) -> Any:
350366
"""
351367
Returns a remote test event
352368
@@ -360,7 +376,7 @@ def get_event(
360376
The name of the registry that contains the schema
361377
Returns
362378
-------
363-
The JSON data of the event
379+
The parsed JSON data and metadata of the event
364380
"""
365381
api_caller = self._api_caller
366382

@@ -377,7 +393,7 @@ def get_event(
377393
event = self.get_event_from_schema(schema, event_name)
378394

379395
try:
380-
return json.dumps(event)
396+
return {"json": json.dumps(event["value"]), "metadata": event.get("x-metadata", {})}
381397

382398
except JSONDecodeError as ex:
383399
LOG.error("Error parsing event %s from schema %s", event_name, schema_name)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
Enums for determining InvocationType of a Lambda function invoke.
3+
"""
4+
5+
EVENT = "Event"
6+
REQUEST_RESPONSE = "RequestResponse"

tests/unit/commands/remote/invoke/test_cli.py

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def test_remote_invoke_with_shared_test_event_command(
147147

148148
context_mock = Mock()
149149
test_event_mock = Mock()
150-
test_event_mock.get_event.return_value = "stuff"
150+
test_event_mock.get_event.return_value = {"json": "stuff", "metadata": {}}
151151
fn_resource = Mock()
152152
fn_resource.resource_type = AWS_LAMBDA_FUNCTION
153153
context_mock.resource_summary = fn_resource
@@ -191,6 +191,142 @@ def mock_tracker(name, value): # when track_event is called, append an equivale
191191
# Assert metric was emitted
192192
self.assertIn(["RemoteInvokeEventType", "remote_event"], tracked_events)
193193

194+
@patch("samcli.lib.remote_invoke.remote_invoke_executors.RemoteInvokeExecutionInfo")
195+
@patch("samcli.lib.utils.boto_utils.get_boto_client_provider_with_config")
196+
@patch("samcli.lib.utils.boto_utils.get_boto_resource_provider_with_config")
197+
@patch("samcli.commands.remote.remote_invoke_context.RemoteInvokeContext")
198+
@patch("samcli.lib.telemetry.event.EventTracker.track_event")
199+
def test_remote_invoke_with_shared_test_event_metadata_command(
200+
self,
201+
mock_track_event,
202+
mock_remote_invoke_context,
203+
patched_get_boto_resource_provider_with_config,
204+
patched_get_boto_client_provider_with_config,
205+
patched_remote_invoke_execution_info,
206+
):
207+
given_client_provider = Mock()
208+
patched_get_boto_client_provider_with_config.return_value = given_client_provider
209+
210+
given_resource_provider = Mock()
211+
patched_get_boto_resource_provider_with_config.return_value = given_resource_provider
212+
213+
given_remote_invoke_execution_info = Mock()
214+
patched_remote_invoke_execution_info.return_value = given_remote_invoke_execution_info
215+
216+
context_mock = Mock()
217+
test_event_mock = Mock()
218+
test_event_mock.get_event.return_value = {"json": "stuff", "metadata": {"invocationType": "Event"}}
219+
fn_resource = Mock()
220+
fn_resource.resource_type = AWS_LAMBDA_FUNCTION
221+
context_mock.resource_summary = fn_resource
222+
context_mock.get_lambda_shared_test_event_provider.return_value = test_event_mock
223+
mock_remote_invoke_context.return_value.__enter__.return_value = context_mock
224+
225+
given_remote_invoke_result = Mock()
226+
given_remote_invoke_result.is_succeeded.return_value = True
227+
given_remote_invoke_result.log_output = "log_output"
228+
context_mock.run.return_value = given_remote_invoke_result
229+
230+
tracked_events = []
231+
232+
def mock_tracker(name, value): # when track_event is called, append an equivalent event to our list
233+
tracked_events.append([name, value])
234+
235+
mock_track_event.side_effect = mock_tracker
236+
237+
do_cli(
238+
stack_name=self.stack_name,
239+
resource_id=self.resource_id,
240+
event=None,
241+
event_file=None,
242+
parameter={},
243+
output=RemoteInvokeOutputFormat.TEXT,
244+
test_event_name="event1",
245+
region=self.region,
246+
profile=self.profile,
247+
config_file=self.config_file,
248+
config_env=self.config_env,
249+
)
250+
251+
test_event_mock.get_event.assert_called_with("event1", fn_resource)
252+
patched_remote_invoke_execution_info.assert_called_with(
253+
payload="stuff",
254+
payload_file=None,
255+
parameters={"InvocationType": "Event"},
256+
output_format=RemoteInvokeOutputFormat.TEXT,
257+
)
258+
context_mock.run.assert_called_with(remote_invoke_input=given_remote_invoke_execution_info)
259+
# Assert metric was emitted
260+
self.assertIn(["RemoteInvokeEventType", "remote_event"], tracked_events)
261+
262+
@patch("samcli.lib.remote_invoke.remote_invoke_executors.RemoteInvokeExecutionInfo")
263+
@patch("samcli.lib.utils.boto_utils.get_boto_client_provider_with_config")
264+
@patch("samcli.lib.utils.boto_utils.get_boto_resource_provider_with_config")
265+
@patch("samcli.commands.remote.remote_invoke_context.RemoteInvokeContext")
266+
@patch("samcli.lib.telemetry.event.EventTracker.track_event")
267+
def test_remote_invoke_with_shared_test_event_metadata_and_cli_overrides_command(
268+
self,
269+
mock_track_event,
270+
mock_remote_invoke_context,
271+
patched_get_boto_resource_provider_with_config,
272+
patched_get_boto_client_provider_with_config,
273+
patched_remote_invoke_execution_info,
274+
):
275+
given_client_provider = Mock()
276+
patched_get_boto_client_provider_with_config.return_value = given_client_provider
277+
278+
given_resource_provider = Mock()
279+
patched_get_boto_resource_provider_with_config.return_value = given_resource_provider
280+
281+
given_remote_invoke_execution_info = Mock()
282+
patched_remote_invoke_execution_info.return_value = given_remote_invoke_execution_info
283+
284+
context_mock = Mock()
285+
test_event_mock = Mock()
286+
test_event_mock.get_event.return_value = {"json": "stuff", "metadata": {"invocationType": "Event"}}
287+
fn_resource = Mock()
288+
fn_resource.resource_type = AWS_LAMBDA_FUNCTION
289+
context_mock.resource_summary = fn_resource
290+
context_mock.get_lambda_shared_test_event_provider.return_value = test_event_mock
291+
mock_remote_invoke_context.return_value.__enter__.return_value = context_mock
292+
293+
given_remote_invoke_result = Mock()
294+
given_remote_invoke_result.is_succeeded.return_value = True
295+
given_remote_invoke_result.log_output = "log_output"
296+
context_mock.run.return_value = given_remote_invoke_result
297+
298+
tracked_events = []
299+
300+
def mock_tracker(name, value): # when track_event is called, append an equivalent event to our list
301+
tracked_events.append([name, value])
302+
303+
mock_track_event.side_effect = mock_tracker
304+
305+
do_cli(
306+
stack_name=self.stack_name,
307+
resource_id=self.resource_id,
308+
event=None,
309+
event_file=None,
310+
parameter={"InvocationType": "RequestResponse"},
311+
output=RemoteInvokeOutputFormat.TEXT,
312+
test_event_name="event1",
313+
region=self.region,
314+
profile=self.profile,
315+
config_file=self.config_file,
316+
config_env=self.config_env,
317+
)
318+
319+
test_event_mock.get_event.assert_called_with("event1", fn_resource)
320+
patched_remote_invoke_execution_info.assert_called_with(
321+
payload="stuff",
322+
payload_file=None,
323+
parameters={"InvocationType": "RequestResponse"},
324+
output_format=RemoteInvokeOutputFormat.TEXT,
325+
)
326+
context_mock.run.assert_called_with(remote_invoke_input=given_remote_invoke_execution_info)
327+
# Assert metric was emitted
328+
self.assertIn(["RemoteInvokeEventType", "remote_event"], tracked_events)
329+
194330
@patch("samcli.lib.remote_invoke.remote_invoke_executors.RemoteInvokeExecutionInfo")
195331
@patch("samcli.lib.utils.boto_utils.get_boto_client_provider_with_config")
196332
@patch("samcli.lib.utils.boto_utils.get_boto_resource_provider_with_config")

0 commit comments

Comments
 (0)