Skip to content

Commit 3a1eb08

Browse files
committed
feat(testing-sdk): add dual-mode integration testing infrastructure
Add comprehensive integration testing infrastructure that supports both local (in-memory) and cloud (AWS Lambda) test execution modes with a unified test interface.
1 parent afb374e commit 3a1eb08

38 files changed

+3135
-176
lines changed

.github/workflows/deploy-examples.yml

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ jobs:
7171
run: hatch run examples:build
7272

7373
- name: Deploy Lambda function - ${{ matrix.example.name }}
74+
id: deploy
7475
env:
7576
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
7677
LAMBDA_ENDPOINT: ${{ secrets.LAMBDA_ENDPOINT_BETA }}
@@ -88,13 +89,35 @@ jobs:
8889
echo "Deploying ${{ matrix.example.name }} as $FUNCTION_NAME"
8990
hatch run examples:deploy "${{ matrix.example.name }}" --function-name "$FUNCTION_NAME"
9091
91-
# $LATEST is also a qualified version
92-
QUALIFIED_FUNCTION_NAME="$FUNCTION_NAME:\$LATEST"
92+
# $LATEST is also a qualified version
93+
QUALIFIED_FUNCTION_NAME="${FUNCTION_NAME}:\$LATEST"
9394
9495
# Store both names for later steps
9596
echo "FUNCTION_NAME=$FUNCTION_NAME" >> $GITHUB_ENV
9697
echo "QUALIFIED_FUNCTION_NAME=$QUALIFIED_FUNCTION_NAME" >> $GITHUB_ENV
9798
echo "VERSION=$VERSION" >> $GITHUB_ENV
99+
echo "DEPLOYED_FUNCTION_NAME=$FUNCTION_NAME" >> $GITHUB_OUTPUT
100+
echo "QUALIFIED_FUNCTION_NAME=$QUALIFIED_FUNCTION_NAME" >> $GITHUB_OUTPUT
101+
102+
- name: Run Integration Tests - ${{ matrix.example.name }}
103+
env:
104+
AWS_REGION: ${{ env.AWS_REGION }}
105+
LAMBDA_ENDPOINT: ${{ secrets.LAMBDA_ENDPOINT_BETA }}
106+
QUALIFIED_FUNCTION_NAME: ${{ env.QUALIFIED_FUNCTION_NAME }}
107+
LAMBDA_FUNCTION_TEST_NAME: ${{ matrix.example.name }}
108+
run: |
109+
echo "Running integration tests for ${{ matrix.example.name }}"
110+
echo "Function name: ${{ steps.deploy.outputs.DEPLOYED_FUNCTION_NAME }}"
111+
echo "Qualified function name: ${QUALIFIED_FUNCTION_NAME}"
112+
echo "AWS Region: ${AWS_REGION}"
113+
echo "Lambda Endpoint: ${LAMBDA_ENDPOINT}"
114+
115+
# Convert example name to test name: "Hello World" -> "test_hello_world"
116+
TEST_NAME="test_$(echo "${{ matrix.example.name }}" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')"
117+
echo "Test name: ${TEST_NAME}"
118+
119+
# Run integration tests
120+
hatch run test:examples-integration
98121
99122
- name: Invoke Lambda function - ${{ matrix.example.name }}
100123
env:

examples/examples-catalog.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,50 @@
110110
"ExecutionTimeout": 300
111111
},
112112
"path": "./src/map_operations.py"
113+
},
114+
{
115+
"name": "Block Example",
116+
"description": "Nested child contexts demonstrating block operations",
117+
"handler": "block_example.handler",
118+
"integration": true,
119+
"durableConfig": {
120+
"RetentionPeriodInDays": 7,
121+
"ExecutionTimeout": 300
122+
},
123+
"path": "./src/block_example.py"
124+
},
125+
{
126+
"name": "Logger Example",
127+
"description": "Demonstrating logger usage and enrichment in DurableContext",
128+
"handler": "logger_example.handler",
129+
"integration": true,
130+
"durableConfig": {
131+
"RetentionPeriodInDays": 7,
132+
"ExecutionTimeout": 300
133+
},
134+
"path": "./src/logger_example.py"
135+
},
136+
{
137+
"name": "Steps with Retry",
138+
"description": "Multiple steps with retry logic in a polling pattern",
139+
"handler": "steps_with_retry.handler",
140+
"integration": true,
141+
"durableConfig": {
142+
"RetentionPeriodInDays": 7,
143+
"ExecutionTimeout": 300
144+
},
145+
"path": "./src/steps_with_retry.py"
146+
},
147+
{
148+
"name": "Wait for Condition",
149+
"description": "Polling pattern that waits for a condition to be met",
150+
"handler": "wait_for_condition.handler",
151+
"integration": true,
152+
"durableConfig": {
153+
"RetentionPeriodInDays": 7,
154+
"ExecutionTimeout": 300
155+
},
156+
"path": "./src/wait_for_condition.py"
113157
}
114158
]
115159
}

examples/src/block_example.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Example demonstrating nested child contexts (blocks)."""
2+
3+
from typing import Any
4+
5+
from aws_durable_execution_sdk_python.context import (
6+
DurableContext,
7+
durable_with_child_context,
8+
)
9+
from aws_durable_execution_sdk_python.execution import durable_execution
10+
11+
12+
@durable_with_child_context
13+
def nested_block(ctx: DurableContext) -> str:
14+
"""Nested block with its own child context."""
15+
# Wait in the nested block
16+
ctx.wait(seconds=1)
17+
return "nested block result"
18+
19+
20+
@durable_with_child_context
21+
def parent_block(ctx: DurableContext) -> dict[str, str]:
22+
"""Parent block with nested operations."""
23+
# Nested step
24+
nested_result: str = ctx.step(
25+
lambda _: "nested step result",
26+
name="nested_step",
27+
)
28+
29+
# Nested block with its own child context
30+
nested_block_result: str = ctx.run_in_child_context(nested_block())
31+
32+
return {
33+
"nestedStep": nested_result,
34+
"nestedBlock": nested_block_result,
35+
}
36+
37+
38+
@durable_execution
39+
def handler(_event: Any, context: DurableContext) -> dict[str, str]:
40+
"""Handler demonstrating nested child contexts."""
41+
# Run parent block which contains nested operations
42+
result: dict[str, str] = context.run_in_child_context(
43+
parent_block(), name="parent_block"
44+
)
45+
46+
return result

examples/src/logger_example.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Example demonstrating logger usage in DurableContext."""
2+
3+
from typing import Any
4+
5+
from aws_durable_execution_sdk_python.context import (
6+
DurableContext,
7+
durable_with_child_context,
8+
)
9+
from aws_durable_execution_sdk_python.execution import durable_execution
10+
11+
12+
@durable_with_child_context
13+
def child_workflow(ctx: DurableContext) -> str:
14+
"""Child workflow with its own logging context."""
15+
# Child context logger has step_id populated with child context ID
16+
ctx.logger.info("Running in child context")
17+
18+
# Step in child context has nested step ID
19+
child_result: str = ctx.step(
20+
lambda _: "child-processed",
21+
name="child_step",
22+
)
23+
24+
ctx.logger.info("Child workflow completed", extra={"result": child_result})
25+
26+
return child_result
27+
28+
29+
@durable_execution
30+
def handler(event: Any, context: DurableContext) -> str:
31+
"""Handler demonstrating logger usage."""
32+
# Top-level context logger: no step_id field
33+
context.logger.info("Starting workflow", extra={"eventId": event.get("id")})
34+
35+
# Logger in steps - gets enriched with step ID and attempt number
36+
result1: str = context.step(
37+
lambda _: "processed",
38+
name="process_data",
39+
)
40+
41+
context.logger.info("Step 1 completed", extra={"result": result1})
42+
43+
# Child contexts inherit the parent's logger and have their own step ID
44+
result2: str = context.run_in_child_context(child_workflow(), name="child_workflow")
45+
46+
context.logger.info(
47+
"Workflow completed", extra={"result1": result1, "result2": result2}
48+
)
49+
50+
return f"{result1}-{result2}"

examples/src/map_operations.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Example demonstrating map-like operations for processing collections durably."""
2+
13
from typing import Any
24

35
from aws_durable_execution_sdk_python.context import DurableContext
@@ -9,8 +11,8 @@ def square(x: int) -> int:
911

1012

1113
@durable_execution
12-
def handler(_event: Any, context: DurableContext) -> str:
13-
# Process a list of items using map-like operations
14+
def handler(_event: Any, context: DurableContext) -> list[int]:
15+
"""Process a list of items using map-like operations."""
1416
items = [1, 2, 3, 4, 5]
1517

1618
# Process each item as a separate durable step
@@ -19,4 +21,4 @@ def handler(_event: Any, context: DurableContext) -> str:
1921
result = context.step(lambda _, x=item: square(x), name=f"square_{i}")
2022
results.append(result)
2123

22-
return f"Squared results: {results}"
24+
return results

examples/src/parallel.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
"""Example demonstrating parallel-like operations for concurrent execution."""
2+
13
from typing import Any
24

35
from aws_durable_execution_sdk_python.context import DurableContext
46
from aws_durable_execution_sdk_python.execution import durable_execution
57

68

79
@durable_execution
8-
def handler(_event: Any, context: DurableContext) -> str:
9-
# Execute multiple operations in parallel
10+
def handler(_event: Any, context: DurableContext) -> list[str]:
11+
# Execute multiple operations
1012
task1 = context.step(lambda _: "Task 1 complete", name="task1")
1113
task2 = context.step(lambda _: "Task 2 complete", name="task2")
1214
task3 = context.step(lambda _: "Task 3 complete", name="task3")
1315

1416
# All tasks execute concurrently and results are collected
15-
return f"Results: {task1}, {task2}, {task3}"
17+
return [task1, task2, task3]

examples/src/step_with_retry.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616

1717
@durable_step
18-
def unreliable_operation(_step_context: StepContext) -> str:
18+
def unreliable_operation(
19+
_step_context: StepContext,
20+
) -> str:
1921
failure_threshold = 0.5
2022
if random() > failure_threshold: # noqa: S311
2123
msg = "Random error occurred"

examples/src/steps_with_retry.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Example demonstrating multiple steps with retry logic."""
2+
3+
from random import random
4+
from typing import Any
5+
6+
from aws_durable_execution_sdk_python.config import StepConfig
7+
from aws_durable_execution_sdk_python.context import DurableContext
8+
from aws_durable_execution_sdk_python.execution import durable_execution
9+
from aws_durable_execution_sdk_python.retries import (
10+
RetryStrategyConfig,
11+
create_retry_strategy,
12+
)
13+
14+
15+
def simulated_get_item(name: str) -> dict[str, Any] | None:
16+
"""Simulate getting an item that may fail randomly."""
17+
# Fail 50% of the time
18+
if random() < 0.5: # noqa: S311
19+
msg = "Random failure"
20+
raise RuntimeError(msg)
21+
22+
# Simulate finding item after some attempts
23+
if random() > 0.3: # noqa: S311
24+
return {"id": name, "data": "item data"}
25+
26+
return None
27+
28+
29+
@durable_execution
30+
def handler(event: Any, context: DurableContext) -> dict[str, Any]:
31+
"""Handler demonstrating polling with retry logic."""
32+
name = event.get("name", "test-item")
33+
34+
# Retry configuration for steps
35+
retry_config = RetryStrategyConfig(
36+
max_attempts=5,
37+
retryable_error_types=[RuntimeError],
38+
)
39+
40+
step_config = StepConfig(create_retry_strategy(retry_config))
41+
42+
item = None
43+
poll_count = 0
44+
max_polls = 5
45+
46+
try:
47+
while poll_count < max_polls:
48+
poll_count += 1
49+
50+
# Try to get the item with retry
51+
get_response = context.step(
52+
lambda _, n=name: simulated_get_item(n),
53+
name=f"get_item_poll_{poll_count}",
54+
config=step_config,
55+
)
56+
57+
# Did we find the item?
58+
if get_response:
59+
item = get_response
60+
break
61+
62+
# Wait 1 second until next poll
63+
context.wait(seconds=1)
64+
65+
except RuntimeError as e:
66+
# Retries exhausted
67+
return {"error": "DDB Retries Exhausted", "message": str(e)}
68+
69+
if not item:
70+
return {"error": "Item Not Found"}
71+
72+
# We found the item!
73+
return {"success": True, "item": item, "pollsRequired": poll_count}

examples/src/wait_for_condition.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Example demonstrating wait-for-condition pattern."""
2+
3+
from typing import Any
4+
5+
from aws_durable_execution_sdk_python.context import DurableContext
6+
from aws_durable_execution_sdk_python.execution import durable_execution
7+
8+
9+
@durable_execution
10+
def handler(_event: Any, context: DurableContext) -> int:
11+
"""Handler demonstrating wait-for-condition pattern."""
12+
state = 0
13+
attempt = 0
14+
max_attempts = 5
15+
16+
while attempt < max_attempts:
17+
attempt += 1
18+
19+
# Execute step to update state
20+
state = context.step(
21+
lambda _, s=state: s + 1,
22+
name=f"increment_state_{attempt}",
23+
)
24+
25+
# Check condition
26+
if state >= 3:
27+
# Condition met, stop
28+
break
29+
30+
# Wait before next attempt
31+
context.wait(seconds=1)
32+
33+
return state

0 commit comments

Comments
 (0)