Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/workflows/conformance-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Conformance Tests

# Full-integration conformance run: discovers suites from template_<suite>.yaml,
# builds the Python handlers against the local monorepo SDK, and runs each suite
# as its own parallel matrix job with the pinned language-agnostic runner.

on:
pull_request:
branches: ["main"]
paths:
- "packages/aws-durable-execution-sdk-python-conformance-tests/**"
- ".github/workflows/conformance-tests.yml"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like Java workflow, sdk changes are ignored.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add the trigger in a following PR that includes all the testing suites

workflow_dispatch:
inputs:
region:
description: "AWS Region for deploying and running conformance tests"
required: false
default: us-west-2
type: string

env:
AWS_REGION: ${{ github.event.inputs.region || 'us-west-2' }}

concurrency:
group: ${{ github.head_ref || github.ref_name || github.run_id }}-conformance
cancel-in-progress: true

permissions:
contents: read

jobs:
discover_suites:
name: discover conformance suites
runs-on: ubuntu-latest
outputs:
suites: ${{ steps.discover.outputs.suites }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Discover suites from templates
id: discover
working-directory: packages/aws-durable-execution-sdk-python-conformance-tests
run: echo "suites=$(python3 scripts/discover_suites.py)" >> "$GITHUB_OUTPUT"

conformance:
name: conformance (${{ matrix.suite }})
needs: discover_suites
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for AWS OIDC credentials
strategy:
fail-fast: false
matrix:
suite: ${{ fromJSON(needs.discover_suites.outputs.suites) }}
defaults:
run:
working-directory: packages/aws-durable-execution-sdk-python-conformance-tests
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: "${{ secrets.TEST_ROLE_ARN }}"
role-session-name: pythonConformanceTest
aws-region: ${{ env.AWS_REGION }}

- name: Verify AWS test account
env:
TEST_ACCOUNT_ID: ${{ secrets.TEST_ACCOUNT_ID }}
run: |
ACTUAL_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
if [ "$ACTUAL_ACCOUNT_ID" != "$TEST_ACCOUNT_ID" ]; then
echo "Expected AWS account $TEST_ACCOUNT_ID but assumed into $ACTUAL_ACCOUNT_ID"
exit 1
fi
echo "Using AWS test account $ACTUAL_ACCOUNT_ID"

- name: Setup SAM CLI
uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3

- name: Build conformance handlers
run: python3 scripts/build_examples.py

- name: Install conformance runner
run: pip install aws-durable-execution-conformance-tests==0.1.0

- name: Inject Lambda execution role into template
env:
ROLE_ARN: ${{ secrets.TEST_LAMBDA_EXECUTION_ROLE_ARN }}
run: |
if [ -z "$ROLE_ARN" ]; then
echo "TEST_LAMBDA_EXECUTION_ROLE_ARN not set; template will create its own role."
exit 0
fi
python3 scripts/inject_execution_role.py \
--template template_${{ matrix.suite }}.yaml \
--role-arn "$ROLE_ARN"

- name: Run conformance suite
run: |
python -m aws_durable_execution_conformance_tests.app \
--template template_${{ matrix.suite }}.yaml \
--language python \
--suite ${{ matrix.suite }} \
--name conf-py-${{ matrix.suite }}-${{ github.run_id }} \
--region ${{ env.AWS_REGION }} \
--history-dir history-${{ matrix.suite }} \
--report junit \
--report-file report-${{ matrix.suite }}

- name: Upload conformance report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: conformance-report-${{ matrix.suite }}
path: |
packages/aws-durable-execution-sdk-python-conformance-tests/report-${{ matrix.suite }}.xml
packages/aws-durable-execution-sdk-python-conformance-tests/history-${{ matrix.suite }}/
if-no-files-found: warn
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
build/
lambda-build/
dist/
.aws-sam/
history-*/
report-*.xml
report-*.json
__pycache__/
*.pyc
121 changes: 121 additions & 0 deletions packages/aws-durable-execution-sdk-python-conformance-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Durable Execution Python SDK — Conformance Tests

Cross-SDK **conformance test handlers** for the Durable Execution Python SDK.
These handlers deploy as AWS Lambda functions and are exercised by the
language-agnostic conformance runner
[`aws-durable-execution-conformance-tests`](https://pypi.org/project/aws-durable-execution-conformance-tests/),
which invokes each function, pulls its execution history, and asserts it matches
the shared requirement specification.

The runner and requirement specifications are maintained in
[`aws/aws-durable-execution-conformance-tests`](https://github.com/aws/aws-durable-execution-conformance-tests).
This package owns the Python handlers and SAM templates that wire them to
requirement IDs.

## Layout

```
handlers/
<suite>/ # one .py handler per conformance scenario
template_<suite>.yaml # maps suite handlers to requirement IDs
scripts/
discover_suites.py # derives supported suites from templates
build_examples.py # assembles lambda-build/ from the local monorepo SDK
inject_execution_role.py # CI: point functions at a pre-existing role
tests/ # unit tests for the scripts
```

Each `template_<suite>.yaml` is a self-contained deployment for one conformance
suite. Templates are the single source of truth: the local build and GitHub
Actions matrix discover suites from them automatically.

## How a handler maps to a requirement

The link is the `TestingMetadata.TestDescription` field on each function in the
SAM template:

```yaml
RequirementCase:
Type: AWS::Serverless::Function
TestingMetadata:
TestDescription: ["<requirement-id>"]
Properties:
CodeUri: lambda-build/
Handler: <suite>.<handler_module>.handler
Role: !GetAtt DurableFunctionRole.Arn
DurableConfig:
RetentionPeriodInDays: 7
ExecutionTimeout: 300
```

The runner invokes the function once per requirement ID using the requirement's
`Input`, then compares the execution history with `ExpectedExecutionHistory`.

## Building locally

`scripts/build_examples.py` assembles `lambda-build/` from the local monorepo
SDK source, not a PyPI release. `boto3` is provided by the Lambda runtime and is
not vendored.

```bash
cd packages/aws-durable-execution-sdk-python-conformance-tests
python3 scripts/build_examples.py
```

The script discovers every `template_<suite>.yaml`, verifies a matching
`handlers/<suite>/` directory exists, and copies all discovered suites:

```
lambda-build/
aws_durable_execution_sdk_python/
<suite>/
```

## Running a suite

Prerequisites: Python ≥ 3.14, the AWS SAM CLI, and AWS credentials for an
account where Durable Execution is available.

```bash
cd packages/aws-durable-execution-sdk-python-conformance-tests
SUITE=<suite>

python3 scripts/build_examples.py
pip install aws-durable-execution-conformance-tests==0.1.0

python -m aws_durable_execution_conformance_tests.app \
--template "template_${SUITE}.yaml" \
--language python \
--suite "$SUITE" \
--name "conformance-python-${SUITE}-local" \
--region us-west-2 \
--history-dir "history-${SUITE}" \
--report junit --report-file "report-${SUITE}"
```

The runner deploys the template, invokes each function, validates its result and
history, and cleans up the stack by default.

## Authoring a new test case

1. Find or add the requirement in the conformance repository under
`test-requirements/<suite>/<id>.yaml`.
2. Add `handlers/<suite>/<descriptive_name>.py` exporting `handler`. Use the
SDK's real API; never hand-roll behavior to force an expected result.
3. Register it in `template_<suite>.yaml` with
`Handler: <suite>.<name>.handler` and `TestDescription: ["<id>"]`.
4. Rebuild and run the suite.

For a new suite, adding its template and handler directory is sufficient; the
build and CI matrix discover it automatically.

## CI

`.github/workflows/conformance-tests.yml` first discovers all suites from
`template_<suite>.yaml`, then runs one parallel matrix job per suite. CI assumes
AWS credentials through the repository's existing OIDC secrets.

Before deployment, `scripts/inject_execution_role.py` points every function at
the pre-existing execution role (`TEST_LAMBDA_EXECUTION_ROLE_ARN`) and removes
the template's self-created `DurableFunctionRole`. The checked-in templates
remain self-contained for local runs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Any

from aws_durable_execution_sdk_python.config import Duration
from aws_durable_execution_sdk_python.context import (
DurableContext,
StepContext,
durable_step,
)
from aws_durable_execution_sdk_python.execution import durable_execution


@durable_step
def compute(_step_context: StepContext) -> str:
return "computed"


@durable_execution
def handler(_event: Any, context: DurableContext) -> str:
result: str = context.step(compute())
context.wait(Duration.from_seconds(2))
return result
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""1-17: AtMostOnce interrupted (no retry) - Lambda crash, StepInterruptedError, fails permanently."""

import os
import time
from typing import Any

from aws_durable_execution_sdk_python.config import StepConfig, StepSemantics
from aws_durable_execution_sdk_python.context import (
DurableContext,
StepContext,
durable_step,
)
from aws_durable_execution_sdk_python.execution import durable_execution
from aws_durable_execution_sdk_python.retries import RetryPresets


@durable_step
def at_most_once_flaky_step(_step_context: StepContext, *, input_1: str) -> str:
print(input_1, flush=True)
time.sleep(1) # Allow time for logs to flush to CloudWatch
os._exit(1) # Simulate Lambda crash
return "unreachable"


@durable_execution
def handler(event: Any, context: DurableContext) -> str:
result: str = context.step(
at_most_once_flaky_step(input_1=str(event)),
name="at_most_once_flaky_step",
config=StepConfig(
retry_strategy=RetryPresets.none(),
step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY,
),
)
return result
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""1-18: AtMostOnce interrupted (with retry, uses the step context attempt number)."""

import os
import time
from typing import Any

from aws_durable_execution_sdk_python.config import Duration, StepConfig, StepSemantics
from aws_durable_execution_sdk_python.context import (
DurableContext,
StepContext,
durable_step,
)
from aws_durable_execution_sdk_python.execution import durable_execution
from aws_durable_execution_sdk_python.retries import (
RetryStrategyConfig,
create_retry_strategy,
)


@durable_step
def at_most_once_step(step_context: StepContext, *, input_1: str) -> str:
# Print input to stdout each time step executes
print(input_1, flush=True)
time.sleep(1) # Allow time for logs to flush to CloudWatch

# The attempt number is the SDK's built-in durable counter from the step
# context (1-based). Under AtMostOncePerRetry the interrupted first attempt
# is consumed, so the retry re-executes as attempt 2.
if step_context.attempt < 2:
# First attempt: simulate Lambda crash
os._exit(1)
# Second attempt (retry): succeed
return "succeeded on second attempt"


@durable_execution
def handler(event: Any, context: DurableContext) -> str:
retry_config = RetryStrategyConfig(
max_attempts=3,
initial_delay=Duration.from_seconds(1),
)

result: str = context.step(
at_most_once_step(input_1=str(event)),
config=StepConfig(
retry_strategy=create_retry_strategy(retry_config),
step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY,
),
)
return result
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Any

from aws_durable_execution_sdk_python.context import (
DurableContext,
StepContext,
durable_step,
)
from aws_durable_execution_sdk_python.execution import durable_execution


@durable_step
def greet(_step_context: StepContext, name: str) -> str:
return f"Hello, {name}!"


@durable_execution
def handler(event: Any, context: DurableContext) -> str:
result: str = context.step(greet(event))
return result
Loading