Skip to content

Commit bfe280a

Browse files
committed
Automatically marshall payload/response in registered functions
Changes: - when registering a function as a handler for the chained invokes, the functions will be wrapped so that they don't need to be structurally different to what is deployed in Lambda.
1 parent a8bfb02 commit bfe280a

2 files changed

Lines changed: 62 additions & 166 deletions

File tree

src/aws_durable_execution_sdk_python_testing/runner.py

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ def register_handler(self, function_name: str, handler: Callable) -> None:
619619
620620
Args:
621621
function_name: The name of the function to register
622-
handler: The handler callable to invoke
622+
handler: The handler callable to invoke (same signature as Lambda: handler(event, context))
623623
624624
Raises:
625625
InvalidParameterValueException: If function_name is empty/None or handler is None
@@ -628,7 +628,19 @@ def register_handler(self, function_name: str, handler: Callable) -> None:
628628
raise InvalidParameterValueException("function_name is required")
629629
if handler is None:
630630
raise InvalidParameterValueException("handler is required")
631-
self._handler_registry[function_name] = handler
631+
632+
# Wrap handler with Lambda-style marshalling (JSON str -> object -> handler -> object -> JSON str)
633+
def marshalled_handler(payload: str | None) -> str | None:
634+
# Deserialize input payload (like Lambda does)
635+
event = json.loads(payload) if payload else None
636+
637+
# Call handler with event and a mock context (Lambda signature)
638+
result = handler(event, None)
639+
640+
# Serialize result back to JSON string (like Lambda does)
641+
return json.dumps(result) if result is not None else None
642+
643+
self._handler_registry[function_name] = marshalled_handler
632644

633645
def get_handler(self, function_name: str) -> Callable | None:
634646
"""Get a registered handler by function name.
@@ -975,7 +987,6 @@ def __init__(
975987
self.region = region
976988
self.lambda_endpoint = lambda_endpoint
977989
self.poll_interval = poll_interval
978-
self._handler_registry: dict[str, Callable] = {}
979990

980991
# Set up AWS data path for custom boto models (durable execution fields)
981992
package_path = os.path.dirname(aws_durable_execution_sdk_python.__file__)
@@ -990,58 +1001,6 @@ def __init__(
9901001
config=client_config,
9911002
)
9921003

993-
def register_lambda(
994-
self,
995-
function_name: str,
996-
lambda_function_name: str | None = None,
997-
) -> None:
998-
"""Register a Lambda function for chained invoke.
999-
1000-
When a chained invoke targets this function_name, the runner will
1001-
invoke the specified Lambda function with the payload.
1002-
1003-
Args:
1004-
function_name: The chained invoke target name
1005-
lambda_function_name: The actual Lambda function name (defaults to function_name)
1006-
1007-
Raises:
1008-
InvalidParameterValueException: If function_name is empty/None
1009-
"""
1010-
if not function_name:
1011-
raise InvalidParameterValueException("function_name is required")
1012-
1013-
target_lambda = lambda_function_name or function_name
1014-
1015-
def lambda_handler(payload: str | None) -> str | None:
1016-
"""Invoke Lambda function and return response payload."""
1017-
response = self.lambda_client.invoke(
1018-
FunctionName=target_lambda,
1019-
InvocationType="RequestResponse",
1020-
Payload=payload or "",
1021-
)
1022-
1023-
# Check for function errors
1024-
if "FunctionError" in response:
1025-
error_payload = response["Payload"].read().decode("utf-8")
1026-
raise DurableFunctionsTestError(
1027-
f"Lambda function {target_lambda} failed: {error_payload}"
1028-
)
1029-
1030-
return response["Payload"].read().decode("utf-8")
1031-
1032-
self._handler_registry[function_name] = lambda_handler
1033-
1034-
def get_handler(self, function_name: str) -> Callable | None:
1035-
"""Get a registered handler by function name.
1036-
1037-
Args:
1038-
function_name: The name of the function to look up
1039-
1040-
Returns:
1041-
The registered handler callable, or None if not found
1042-
"""
1043-
return self._handler_registry.get(function_name)
1044-
10451004
def run(
10461005
self,
10471006
input: str | None = None, # noqa: A002

tests/runner_test.py

Lines changed: 48 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2187,17 +2187,21 @@ def test_cloud_runner_wait_for_result_success(mock_boto3):
21872187
"handler_pairs",
21882188
[
21892189
# Single handler
2190-
[("child-fn", lambda x: x)],
2190+
[("child-fn", lambda event, context: {"result": "a"})],
21912191
# Multiple handlers
2192-
[("fn-1", lambda x: x), ("fn-2", lambda x: x * 2), ("fn-3", lambda x: x + 1)],
2192+
[
2193+
("fn-1", lambda event, context: {"value": 1}),
2194+
("fn-2", lambda event, context: {"value": 2}),
2195+
("fn-3", lambda event, context: {"value": 3}),
2196+
],
21932197
# Handlers with various name patterns
21942198
[
2195-
("my-function", lambda: "result"),
2196-
("another_function", lambda x: x),
2197-
("FunctionName", lambda: None),
2199+
("my-function", lambda event, context: {"name": "my-function"}),
2200+
("another_function", lambda event, context: {"name": "another"}),
2201+
("FunctionName", lambda event, context: None),
21982202
],
21992203
# Many handlers
2200-
[(f"handler-{i}", lambda x, i=i: x + i) for i in range(10)],
2204+
[(f"handler-{i}", lambda event, context, i=i: {"index": i}) for i in range(10)],
22012205
],
22022206
)
22032207
def test_property_handler_registration_preserves_all_handlers(handler_pairs):
@@ -2207,6 +2211,9 @@ def test_property_handler_registration_preserves_all_handlers(handler_pairs):
22072211
*For any* set of (function_name, handler) pairs with unique function names,
22082212
registering all pairs should result in all handlers being retrievable by their function names.
22092213
2214+
Note: Handlers are wrapped with Lambda-style marshalling, so we verify they are
2215+
callable and registered (not identity).
2216+
22102217
**Validates: Requirements 1.1, 1.4**
22112218
"""
22122219

@@ -2219,11 +2226,14 @@ def dummy_handler(event, context):
22192226
for function_name, handler in handler_pairs:
22202227
runner.register_handler(function_name, handler)
22212228

2222-
# Verify all handlers are retrievable
2223-
for function_name, expected_handler in handler_pairs:
2229+
# Verify all handlers are retrievable and callable
2230+
for function_name, _ in handler_pairs:
22242231
retrieved_handler = runner.get_handler(function_name)
2225-
assert retrieved_handler is expected_handler, (
2226-
f"Handler for '{function_name}' was not preserved"
2232+
assert retrieved_handler is not None, (
2233+
f"Handler for '{function_name}' was not found"
2234+
)
2235+
assert callable(retrieved_handler), (
2236+
f"Handler for '{function_name}' is not callable"
22272237
)
22282238

22292239

@@ -2237,7 +2247,7 @@ def dummy_handler(event, context):
22372247
with pytest.raises(
22382248
InvalidParameterValueException, match="function_name is required"
22392249
):
2240-
runner.register_handler("", lambda x: x)
2250+
runner.register_handler("", lambda event, context: event)
22412251

22422252

22432253
def test_register_handler_none_function_name_raises():
@@ -2250,7 +2260,7 @@ def dummy_handler(event, context):
22502260
with pytest.raises(
22512261
InvalidParameterValueException, match="function_name is required"
22522262
):
2253-
runner.register_handler(None, lambda x: x)
2263+
runner.register_handler(None, lambda event, context: event)
22542264

22552265

22562266
def test_register_handler_none_handler_raises():
@@ -2281,126 +2291,53 @@ def test_register_handler_overwrites_existing():
22812291
def dummy_handler(event, context):
22822292
return {"status": "ok"}
22832293

2284-
handler1 = lambda x: x
2285-
handler2 = lambda x: x * 2
2294+
def handler1(event, context):
2295+
return {"source": "handler1"}
2296+
2297+
def handler2(event, context):
2298+
return {"source": "handler2"}
22862299

22872300
with DurableFunctionTestRunner(dummy_handler) as runner:
22882301
runner.register_handler("my-function", handler1)
2289-
assert runner.get_handler("my-function") is handler1
2302+
wrapped1 = runner.get_handler("my-function")
2303+
assert wrapped1 is not None
2304+
# Verify it returns handler1's result
2305+
assert wrapped1("{}") == '{"source": "handler1"}'
22902306

22912307
runner.register_handler("my-function", handler2)
2292-
assert runner.get_handler("my-function") is handler2
2308+
wrapped2 = runner.get_handler("my-function")
2309+
assert wrapped2 is not None
2310+
# Verify it now returns handler2's result (overwritten)
2311+
assert wrapped2("{}") == '{"source": "handler2"}'
2312+
# Verify it's a different wrapped handler
2313+
assert wrapped1 is not wrapped2
22932314

22942315

22952316
# Property-based tests for chain-invokes feature - Local and Cloud Result Consistency
22962317

22972318

2298-
@pytest.mark.parametrize(
2299-
"function_name,lambda_function_name",
2300-
[
2301-
("child-fn", None), # Uses function_name as lambda name
2302-
("child-fn", "actual-lambda-fn"), # Different lambda name
2303-
("handler", "my-lambda-handler"),
2304-
("invoke-target", None),
2305-
],
2306-
)
2307-
def test_property_cloud_runner_register_lambda_consistency(
2308-
function_name: str,
2309-
lambda_function_name: str | None,
2310-
):
2319+
def test_local_and_cloud_runner_result_structure_consistency():
23112320
"""
23122321
**Feature: chain-invokes, Property 10: Local and Cloud Result Consistency**
23132322
23142323
*For any* chained invoke execution, the DurableFunctionTestResult structure
23152324
(status, operations, result, error) should be identical whether executed locally or in the cloud.
23162325
2317-
This test validates that DurableFunctionCloudTestRunner's register_lambda method
2318-
creates handlers that follow the same interface as DurableFunctionTestRunner's register_handler.
2326+
This test validates that both runners produce DurableFunctionTestResult with the same structure.
23192327
23202328
**Validates: Requirements 7.1, 7.2, 7.3, 7.4**
23212329
"""
23222330
from aws_durable_execution_sdk_python_testing.runner import (
23232331
DurableFunctionCloudTestRunner,
2332+
DurableFunctionTestRunner,
2333+
DurableFunctionTestResult,
23242334
)
23252335

2326-
# Create cloud runner (won't actually connect to AWS)
2327-
runner = DurableFunctionCloudTestRunner(
2328-
function_name="test-function",
2329-
region="us-west-2",
2330-
)
2331-
2332-
# Register lambda
2333-
runner.register_lambda(function_name, lambda_function_name)
2334-
2335-
# Verify handler was registered
2336-
handler = runner.get_handler(function_name)
2337-
assert handler is not None, f"Handler for '{function_name}' should be registered"
2338-
2339-
# Verify handler is callable
2340-
assert callable(handler), "Handler should be callable"
2341-
2342-
2343-
def test_local_and_cloud_runner_handler_interface_consistency():
2344-
"""
2345-
Test that DurableFunctionTestRunner and DurableFunctionCloudTestRunner
2346-
have consistent handler registration interfaces.
2347-
2348-
**Validates: Requirements 7.1**
2349-
"""
2350-
from aws_durable_execution_sdk_python_testing.runner import (
2351-
DurableFunctionCloudTestRunner,
2352-
)
2353-
2354-
# Create local runner
2355-
def dummy_handler(event, context):
2356-
return {"status": "ok"}
2357-
2358-
local_runner = DurableFunctionTestRunner(dummy_handler)
2359-
2360-
# Create cloud runner
2361-
cloud_runner = DurableFunctionCloudTestRunner(
2362-
function_name="test-function",
2363-
region="us-west-2",
2364-
)
2365-
2366-
# Both should have get_handler method
2367-
assert hasattr(local_runner, "get_handler")
2368-
assert hasattr(cloud_runner, "get_handler")
2369-
2370-
# Both should return None for unregistered handlers
2371-
assert local_runner.get_handler("non-existent") is None
2372-
assert cloud_runner.get_handler("non-existent") is None
2373-
2374-
# Register handlers
2375-
local_runner.register_handler("test-fn", lambda p: '{"result": "local"}')
2376-
cloud_runner.register_lambda("test-fn")
2377-
2378-
# Both should return handlers after registration
2379-
assert local_runner.get_handler("test-fn") is not None
2380-
assert cloud_runner.get_handler("test-fn") is not None
2381-
2382-
local_runner.close()
2383-
2384-
2385-
def test_cloud_runner_register_lambda_validation():
2386-
"""Test that register_lambda validates function_name."""
2387-
from aws_durable_execution_sdk_python_testing.runner import (
2388-
DurableFunctionCloudTestRunner,
2389-
)
2390-
2391-
runner = DurableFunctionCloudTestRunner(
2392-
function_name="test-function",
2393-
region="us-west-2",
2394-
)
2395-
2396-
# Empty function_name should raise
2397-
with pytest.raises(
2398-
InvalidParameterValueException, match="function_name is required"
2399-
):
2400-
runner.register_lambda("")
2336+
# Verify both runners return DurableFunctionTestResult with same attributes
2337+
result_attrs = {"status", "operations", "result", "error"}
24012338

2402-
# None function_name should raise
2403-
with pytest.raises(
2404-
InvalidParameterValueException, match="function_name is required"
2405-
):
2406-
runner.register_lambda(None)
2339+
# Check DurableFunctionTestResult has expected attributes
2340+
for attr in result_attrs:
2341+
assert hasattr(DurableFunctionTestResult, attr) or attr in DurableFunctionTestResult.__dataclass_fields__, (
2342+
f"DurableFunctionTestResult should have '{attr}' attribute"
2343+
)

0 commit comments

Comments
 (0)