-
Notifications
You must be signed in to change notification settings - Fork 504
Add durable execution conformance test harness (.NET, step suite) #2517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
GarrettBeatty
wants to merge
4
commits into
dev
Choose a base branch
from
feature/durable-conformance-tests
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
4d13ee5
Add durable execution conformance test harness (.NET, step suite)
GarrettBeatty 8d7d366
Port remaining 8 conformance suites (.NET) — all 9 suites green
GarrettBeatty 545a9e5
Default conformance region to us-west-2 to match CI accounts
GarrettBeatty 259395b
Gate conformance CI on missing coverage (--fail-on failed+uncovered)
GarrettBeatty File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| name: Durable Execution Conformance Tests | ||
|
|
||
| # Full-integration conformance run for the .NET Durable Execution SDK: publishes | ||
| # the .NET handlers, installs the language-agnostic runner from the | ||
| # aws-durable-execution-conformance-tests repo, then deploys + invokes + | ||
| # validates one SAM stack per suite. Suites are discovered from the | ||
| # template_<suite>.yaml files under the Conformance directory (see | ||
| # scripts/discover_suites.py) and each runs as its own parallel matrix job, so | ||
| # adding a suite only requires shipping its template + handlers. | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [dev, master] | ||
| paths: | ||
| - "Libraries/src/Amazon.Lambda.DurableExecution/**" | ||
| - "Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Conformance/**" | ||
| - ".github/workflows/conformance-tests.yml" | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: ${{ github.head_ref || github.ref_name || github.run_id }}-conformance | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
| id-token: write # Required for AWS OIDC credentials | ||
|
|
||
| env: | ||
| CONFORMANCE_DIR: Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Conformance | ||
| RUNNER_PIP_SPEC: "git+https://github.com/aws/aws-durable-execution-conformance-tests.git@main#subdirectory=packages/aws-durable-execution-conformance-tests" | ||
|
|
||
| jobs: | ||
| discover_suites: | ||
| name: discover conformance suites | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| suites: ${{ steps.discover.outputs.suites }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Discover suites from templates | ||
| id: discover | ||
| working-directory: ${{ env.CONFORMANCE_DIR }} | ||
| run: echo "suites=$(python3 scripts/discover_suites.py)" >> "$GITHUB_OUTPUT" | ||
|
|
||
| conformance: | ||
| name: conformance (${{ matrix.suite }}) | ||
| needs: discover_suites | ||
| runs-on: ubuntu-latest | ||
| # Global lock per suite stack: runs from different branches/PRs share the | ||
| # persistent conformance-dotnet-<suite> stacks, so deploys to the same stack | ||
| # must never overlap. Queued (not cancelled) so every run still executes. | ||
| concurrency: | ||
| group: conformance-stack-${{ matrix.suite }} | ||
| cancel-in-progress: false | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| suite: ${{ fromJSON(needs.discover_suites.outputs.suites) }} | ||
| defaults: | ||
| run: | ||
| working-directory: Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Conformance | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup .NET | ||
| uses: actions/setup-dotnet@v4 | ||
| with: | ||
| dotnet-version: "8.0.x" | ||
|
|
||
| - name: Setup Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.14" | ||
|
|
||
| - name: Setup SAM CLI | ||
| uses: aws-actions/setup-sam@v2 | ||
| with: | ||
| use-installer: true | ||
|
|
||
| - name: Publish conformance handlers | ||
| run: ./scripts/build_examples.sh ${{ matrix.suite }} | ||
|
|
||
| - name: Install conformance runner | ||
| run: pip install "${RUNNER_PIP_SPEC}" | ||
|
|
||
| - name: Get AWS Credentials | ||
| uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 | ||
| with: | ||
| # SAM-capable deploy role (CloudFormation / S3 / IAM / Lambda / DynamoDB). | ||
| role-to-assume: ${{ secrets.CONFORMANCE_DEPLOY_ROLE_ARN }} | ||
| role-session-name: githubConformanceTest | ||
| aws-region: ${{ vars.CONFORMANCE_AWS_REGION || 'us-west-2' }} | ||
|
|
||
| - name: Inject Lambda execution role into template | ||
| env: | ||
| ROLE_ARN: ${{ secrets.CONFORMANCE_LAMBDA_EXECUTION_ROLE_ARN }} | ||
| run: | | ||
| if [ -z "$ROLE_ARN" ]; then | ||
| echo "CONFORMANCE_LAMBDA_EXECUTION_ROLE_ARN not set; template will create its own role." | ||
| exit 0 | ||
| fi | ||
| # Point every function at the pre-existing execution role and drop the | ||
| # self-created DurableFunctionRole. Mutates only the CI checkout; the | ||
| # checked-in template stays self-contained for local runs. | ||
| python3 scripts/inject_execution_role.py \ | ||
| --template template_${{ matrix.suite }}.yaml \ | ||
| --role-arn "$ROLE_ARN" | ||
|
|
||
| - name: Compute stack-safe suite slug | ||
| run: | | ||
| # CloudFormation stack names allow only [a-zA-Z][-a-zA-Z0-9]*; | ||
| # suite names like wait_for_condition contain underscores. | ||
| echo "SUITE_SLUG=$(echo '${{ matrix.suite }}' | tr '_' '-')" >> "$GITHUB_ENV" | ||
|
|
||
| - name: Run conformance suite | ||
| # --fail-on failed+uncovered: the runner (and its test-requirements) are | ||
| # pinned to @main, so when upstream adds a new requirement to a suite it | ||
| # is pulled automatically. Without this, a new requirement with no .NET | ||
| # handler reports UNCOVERED and the run stays green — silently missing | ||
| # coverage. failed+uncovered turns that red so we notice and either add a | ||
| # handler or declare it under TestingMetadata.NotImplemented. Declared | ||
| # gaps report NOT_IMPLEMENTED, which never blocks. | ||
| run: | | ||
| python -m aws_durable_execution_conformance_tests.app \ | ||
| --template template_${{ matrix.suite }}.yaml \ | ||
| --language dotnet \ | ||
| --suite ${{ matrix.suite }} \ | ||
| --name conformance-dotnet-${SUITE_SLUG} \ | ||
| --region ${{ vars.CONFORMANCE_AWS_REGION || 'us-west-2' }} \ | ||
| --history-dir history-${{ matrix.suite }} \ | ||
| --report junit \ | ||
| --report-file report-${{ matrix.suite }} \ | ||
| --fail-on failed+uncovered \ | ||
| --no-cleanup | ||
|
|
||
| - name: Upload conformance report | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: conformance-report-${{ matrix.suite }} | ||
| path: | | ||
| ${{ env.CONFORMANCE_DIR }}/report-${{ matrix.suite }}.xml | ||
| ${{ env.CONFORMANCE_DIR }}/history-${{ matrix.suite }}/ | ||
| if-no-files-found: warn | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Conformance/.gitignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Published handler artifacts produced by scripts/build_examples.sh | ||
| publish/ | ||
|
|
||
| # Conformance runner output | ||
| history-*/ | ||
| report-*.xml | ||
| report-*.json | ||
|
|
||
| # SAM build/deploy scratch | ||
| .aws-sam/ | ||
| samconfig.toml | ||
|
|
||
| # .NET build output | ||
| **/bin/ | ||
| **/obj/ |
157 changes: 157 additions & 0 deletions
157
...ries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Conformance/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| # Durable Execution Conformance Tests (.NET) | ||
|
|
||
| This directory wires the .NET Durable Execution SDK into the language-neutral | ||
| [`aws-durable-execution-conformance-tests`](https://github.com/aws/aws-durable-execution-conformance-tests) | ||
| runner. The runner is a Python tool that deploys a SAM template, invokes each | ||
| mapped Lambda, and validates the durable execution **result** and **event | ||
| history** against language-agnostic requirement specs. | ||
|
|
||
| ## How it works | ||
|
|
||
| - Each requirement (e.g. `1-1`) has a YAML spec in the runner's | ||
| `test-requirements/<suite>/` directory describing the expected result and | ||
| execution history. | ||
| - For every requirement we implement, there is a small handler project under | ||
| `<suite>/<HandlerName>/` (executable model: `Main` + `LambdaBootstrap`, | ||
| `AssemblyName=bootstrap`). Each project references the in-repo SDK directly. | ||
| - `template_<suite>.yaml` maps each function to its requirement id(s) via | ||
| `TestingMetadata.TestDescription: ["1-1"]` and deploys it on the `dotnet8` | ||
| managed runtime. | ||
| - The runner reads `TestingMetadata`, deploys the template, invokes each | ||
| function (sync or async depending on the requirement), then asserts. | ||
|
|
||
| Handlers are published ahead of time into `publish/<HandlerName>/`; the SAM | ||
| template's `BuildMethod: makefile` copies the pre-built `bootstrap` into the | ||
| deploy artifact. | ||
|
|
||
| ## Layout | ||
|
|
||
| ``` | ||
| Conformance/ | ||
| ├── README.md | ||
| ├── template_step.yaml # one template per suite; functions -> requirement ids | ||
| ├── scripts/ | ||
| │ ├── build_examples.sh # dotnet publish each handler -> publish/<Fn>/ | ||
| │ ├── discover_suites.py # emits the CI matrix (suites with template + handlers) | ||
| │ └── inject_execution_role.py# CI: point functions at a pre-existing role | ||
| └── step/ # one dir per suite; one subdir per handler | ||
| ├── StepBasic/ # 1-1 | ||
| ├── StepWithName/ # 1-2 | ||
| └── ... # 1-3 .. 1-20 | ||
| ``` | ||
|
|
||
| ## Coverage | ||
|
|
||
| All nine suites are implemented (one handler project per requirement id): | ||
|
|
||
| | Suite | Ids | Handlers | | ||
| |-------|-----|----------| | ||
| | `step` | 1-1 .. 1-20 | 20 | | ||
| | `wait` | 2-1 .. 2-5 | 5 | | ||
| | `child` | 3-1 .. 3-13, 3-15 .. 3-18 | 17 | | ||
| | `callback` | 4-1 .. 4-19 | 19 | | ||
| | `invoke` | 5-1 .. 5-15 | 15 (+2 target functions, +1 tenancy alias) | | ||
| | `wait_for_condition` | 6-1 .. 6-13 | 13 | | ||
| | `wait_for_callback` | 7-1 .. 7-15 | 15 | | ||
| | `parallel` | 8-1 .. 8-22 (8-15 n/a) | 21 | | ||
| | `map` | 9-1 .. 9-18 (9-14 n/a) | 17 | | ||
|
|
||
| A few requirement ids have no .NET handler because the SDK intentionally lacks | ||
| the feature they exercise (e.g. per-item / whole-result serdes slots in `map`); | ||
| those are documented in the relevant `template_<suite>.yaml` and reported as | ||
| `NOT_IMPLEMENTED` (non-blocking) rather than silently omitted. | ||
|
|
||
| ### Handlers that need extra resources | ||
|
|
||
| - **Retry-across-invocation tests** (`step` 1-11/1-13/1-14/1-15/1-18, `child` | ||
| 3-7/3-12) count attempts across separate invocations, which the replay model | ||
| cannot hold in memory, so they use the `AttemptsTable` DynamoDB table declared | ||
| in the template (`AWSSDK.DynamoDBv2`). | ||
| - **`invoke` targets** — the suite deploys two callee functions | ||
| (`InvokeEchoTarget`, `InvokeFailTarget`) that the workflow handlers invoke via | ||
| `AWSSDK.Lambda`; ARNs are wired through env vars with `Fn::GetAtt`. The | ||
| tenancy test (5-8) reuses the echo target's binary under a second logical id | ||
| (`InvokeEchoTargetTenant`, `PER_TENANT` isolation) — `build_examples.sh` | ||
| produces that publish dir by aliasing (there is no separate source project). | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - .NET 8 SDK | ||
| - Python 3.14+ and the conformance runner: | ||
| ```bash | ||
| pip install "git+https://github.com/aws/aws-durable-execution-conformance-tests.git@main#subdirectory=packages/aws-durable-execution-conformance-tests" | ||
| ``` | ||
| - [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) | ||
| - AWS credentials for an account allowed to deploy + invoke (CloudFormation, IAM, | ||
| Lambda, DynamoDB). Prefix commands with `unset AWS_PROFILE` to use `[default]`. | ||
|
|
||
| ## Running locally | ||
|
|
||
| From this directory (swap `step` for any suite name): | ||
|
|
||
| ```bash | ||
| # 1. Publish the suite's handlers into publish/<Fn>/ | ||
| # (omit the arg to publish every suite) | ||
| ./scripts/build_examples.sh step | ||
|
|
||
| # 2. Deploy + invoke + validate the suite | ||
| unset AWS_PROFILE | ||
| python -m aws_durable_execution_conformance_tests.app \ | ||
| --template template_step.yaml \ | ||
| --language dotnet \ | ||
| --suite step \ | ||
| --name conformance-dotnet-step \ | ||
| --region us-east-1 \ | ||
| --history-dir history-step \ | ||
| --report console | ||
| ``` | ||
|
|
||
| > On Windows, set `PYTHONUTF8=1` — the runner prints `✅`/`❌`, which crashes the | ||
| > summary printer under the default cp1252 console encoding. | ||
|
|
||
| The checked-in template is self-contained (it creates its own | ||
| `DurableFunctionRole`). CI instead injects a pre-existing execution role with | ||
| `scripts/inject_execution_role.py`. | ||
|
|
||
| ## CI | ||
|
|
||
| `.github/workflows/conformance-tests.yml` runs one matrix job per discovered | ||
| suite: publish handlers → install the runner → assume the deploy role via OIDC → | ||
| inject the execution role → run the suite → upload the JUnit report. It requires | ||
| the repository secret `CONFORMANCE_DEPLOY_ROLE_ARN` (a SAM-capable deploy role; | ||
| provisioned by the `aws-dotnet-ci` CDK), and optionally | ||
| `CONFORMANCE_LAMBDA_EXECUTION_ROLE_ARN` (a pre-created Lambda execution role) and | ||
| the `CONFORMANCE_AWS_REGION` variable (defaults to `us-west-2`). | ||
|
|
||
| ### Coverage gate (keeping up with upstream) | ||
|
|
||
| The runner and its `test-requirements/` are pinned to | ||
| [`aws-durable-execution-conformance-tests@main`](https://github.com/aws/aws-durable-execution-conformance-tests), | ||
| so **new upstream requirements are pulled automatically** on every run. CI runs | ||
| with `--fail-on failed+uncovered`, so a newly-added requirement that has no .NET | ||
| handler reports `UNCOVERED` and **turns the run red** — that's the signal to add | ||
| a handler (or declare it `NotImplemented`). Without that flag the default only | ||
| blocks on `FAILED`, and missing coverage would pass silently. Requirements | ||
| declared under `TestingMetadata.NotImplemented` report `NOT_IMPLEMENTED`, which | ||
| never blocks — so intentional SDK gaps stay green while genuinely-new | ||
| requirements fail loudly. | ||
|
|
||
| ## Adding a suite | ||
|
|
||
| 1. Add `<suite>/<HandlerName>/` handler projects (one per requirement). | ||
| 2. Add `template_<suite>.yaml` mapping each function to its requirement id(s). | ||
| 3. Declare any intentional gaps under a function's | ||
| `TestingMetadata.NotImplemented` (reported `NOT_IMPLEMENTED`, non-blocking). | ||
|
|
||
| `discover_suites.py` picks it up automatically, so it becomes a new CI matrix job. | ||
|
|
||
| ## When CI goes red on a new upstream requirement | ||
|
|
||
| `--fail-on failed+uncovered` means an `UNCOVERED` requirement fails the run. | ||
| When that happens, for the reported id (e.g. a new `1-21`): | ||
|
|
||
| 1. Read its spec in the runner's `test-requirements/<suite>/<id>.yaml`. | ||
| 2. Either **add a handler** — a new `<suite>/<Name>/` project + a resource in | ||
| `template_<suite>.yaml` with `TestDescription: ["<id>"]` — or, if the .NET SDK | ||
| genuinely can't satisfy it, **declare it** under any function's | ||
| `TestingMetadata.NotImplemented` with a reason. |
19 changes: 19 additions & 0 deletions
19
...xecution.IntegrationTests/Conformance/callback/CallbackAfterWait/CallbackAfterWait.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this code was all approved already in the internal git farm repo by alex |
||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <OutputType>Exe</OutputType> | ||
| <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles> | ||
| <AssemblyName>bootstrap</AssemblyName> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="../../../../../src/Amazon.Lambda.Core/Amazon.Lambda.Core.csproj" /> | ||
| <ProjectReference Include="../../../../../src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj" /> | ||
| <ProjectReference Include="../../../../../src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj" /> | ||
| <ProjectReference Include="../../../../../src/Amazon.Lambda.Serialization.SystemTextJson/Amazon.Lambda.Serialization.SystemTextJson.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
31 changes: 31 additions & 0 deletions
31
...mbda.DurableExecution.IntegrationTests/Conformance/callback/CallbackAfterWait/Function.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // 4-9: CreateCallback then wait then await callback | ||
| using Amazon.Lambda.Core; | ||
| using Amazon.Lambda.DurableExecution; | ||
| using Amazon.Lambda.RuntimeSupport; | ||
| using Amazon.Lambda.Serialization.SystemTextJson; | ||
|
|
||
| namespace CallbackAfterWait; | ||
|
|
||
| public class Function | ||
| { | ||
| public static async Task Main(string[] args) | ||
| { | ||
| var handler = new Function(); | ||
| var serializer = new DefaultLambdaJsonSerializer(); | ||
| using var handlerWrapper = HandlerWrapper.GetHandlerWrapper<DurableExecutionInvocationInput, DurableExecutionInvocationOutput>(handler.Handler, serializer); | ||
| using var bootstrap = new LambdaBootstrap(handlerWrapper); | ||
| await bootstrap.RunAsync(); | ||
| } | ||
|
|
||
| public Task<DurableExecutionInvocationOutput> Handler( | ||
| DurableExecutionInvocationInput input, ILambdaContext context) | ||
| => DurableFunction.WrapAsync<string, string>(Workflow, input, context); | ||
|
|
||
| private async Task<string> Workflow(string input, IDurableContext context) | ||
| { | ||
| var callback = await context.CreateCallbackAsync<string>(name: input); | ||
| await context.WaitAsync(TimeSpan.FromSeconds(5)); | ||
| var result = await callback.GetResultAsync(); | ||
| return result; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will configure all of this after this pr is merged. i ran the conformance test locally and it passes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think i may need to modify this slightly to work with the existing buildspec etc but ill do that in a follow up pr