From f1c408df378418d1426ac506688be0d263ac9da5 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Thu, 23 Jul 2026 22:35:37 +0000 Subject: [PATCH 1/7] Add plugin conformance suite handlers (requirements 10-1..10-7) 7 handlers + shared ConformanceLoggingPlugin/FaultyConformancePlugin exercising DurableExecutionPlugin lifecycle hooks via DurableConfig withPlugins, plus template_plugin.yaml. Validated live: 6/7 PASS; 10-3 OPTIONAL_FAILED surfaces a tracked SDK bug (UserFunctionEndInfo succeeded=true for failed attempts). --- .../java/plugin/ConformanceLoggingPlugin.java | 72 ++++++++ .../java/plugin/FaultyConformancePlugin.java | 70 ++++++++ .../java/plugin/PluginAttemptHooksRetry.java | 50 ++++++ .../java/plugin/PluginErrorIsolation.java | 28 +++ .../plugin/PluginFirstInvocationFlag.java | 32 ++++ .../plugin/PluginInvocationLifecycle.java | 33 ++++ .../java/plugin/PluginMultiplePlugins.java | 32 ++++ .../java/plugin/PluginOperationLifecycle.java | 30 ++++ .../java/plugin/PluginTerminalFailure.java | 40 +++++ conformance-tests/template_plugin.yaml | 166 ++++++++++++++++++ 10 files changed, 553 insertions(+) create mode 100644 conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java create mode 100644 conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java create mode 100644 conformance-tests/src/main/java/plugin/PluginAttemptHooksRetry.java create mode 100644 conformance-tests/src/main/java/plugin/PluginErrorIsolation.java create mode 100644 conformance-tests/src/main/java/plugin/PluginFirstInvocationFlag.java create mode 100644 conformance-tests/src/main/java/plugin/PluginInvocationLifecycle.java create mode 100644 conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java create mode 100644 conformance-tests/src/main/java/plugin/PluginOperationLifecycle.java create mode 100644 conformance-tests/src/main/java/plugin/PluginTerminalFailure.java create mode 100644 conformance-tests/template_plugin.yaml diff --git a/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java b/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java new file mode 100644 index 000000000..bcc0c5420 --- /dev/null +++ b/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import software.amazon.lambda.durable.plugin.DurableExecutionPlugin; +import software.amazon.lambda.durable.plugin.InvocationEndInfo; +import software.amazon.lambda.durable.plugin.InvocationInfo; +import software.amazon.lambda.durable.plugin.OperationEndInfo; +import software.amazon.lambda.durable.plugin.OperationInfo; +import software.amazon.lambda.durable.plugin.UserFunctionEndInfo; +import software.amazon.lambda.durable.plugin.UserFunctionStartInfo; + +/** + * Shared instrumentation plugin for the plugin conformance suite. + * + *

Emits lifecycle log lines with a configurable prefix (e.g. {@code CONFPLUGIN}, {@code CONFPLUGIN-A}) so one + * plugin — or two, for the multiple-plugins case — can be registered on a handler. Operation- and attempt-level hooks + * are filtered to step-type operations to match the requirement vocabulary. All lines are emitted from the real SDK + * plugin hooks; nothing is hand-rolled. + */ +@SuppressWarnings("deprecation") +public class ConformanceLoggingPlugin implements DurableExecutionPlugin { + + private final String prefix; + + public ConformanceLoggingPlugin(String prefix) { + this.prefix = prefix; + } + + private static boolean isStep(String type) { + return "STEP".equals(type); + } + + @Override + public void onInvocationStart(InvocationInfo info) { + System.out.println(prefix + " invocation-start first=" + info.isFirstInvocation()); + } + + @Override + public void onInvocationEnd(InvocationEndInfo info) { + System.out.println(prefix + " invocation-end status=" + info.invocationStatus().name()); + } + + @Override + public void onOperationStart(OperationInfo info) { + if (isStep(info.type())) { + System.out.println(prefix + " operation-start"); + } + } + + @Override + public void onOperationEnd(OperationEndInfo info) { + if (isStep(info.type())) { + System.out.println(prefix + " operation-end status=" + info.status()); + } + } + + @Override + public void onUserFunctionStart(UserFunctionStartInfo info) { + if (isStep(info.type()) && info.attempt() != null) { + System.out.println(prefix + " attempt-start n=" + info.attempt()); + } + } + + @Override + public void onUserFunctionEnd(UserFunctionEndInfo info) { + if (isStep(info.type()) && info.attempt() != null) { + String outcome = info.succeeded() ? "SUCCEEDED" : "FAILED"; + System.out.println(prefix + " attempt-end n=" + info.attempt() + " outcome=" + outcome); + } + } +} diff --git a/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java b/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java new file mode 100644 index 000000000..baf5c96c7 --- /dev/null +++ b/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import software.amazon.lambda.durable.plugin.DurableExecutionPlugin; +import software.amazon.lambda.durable.plugin.InvocationEndInfo; +import software.amazon.lambda.durable.plugin.InvocationInfo; +import software.amazon.lambda.durable.plugin.OperationEndInfo; +import software.amazon.lambda.durable.plugin.OperationInfo; +import software.amazon.lambda.durable.plugin.UserFunctionEndInfo; +import software.amazon.lambda.durable.plugin.UserFunctionStartInfo; + +/** + * Instrumentation plugin whose every hook logs a line and then throws. + * + *

Used by requirement 10-4 to verify the SDK isolates plugin exceptions: each hook must run (log its line) and the + * thrown exception must be swallowed by the SDK so the execution result and history are identical to running without + * the plugin. Operation- and attempt-level hooks are filtered to step-type operations. + */ +@SuppressWarnings("deprecation") +public class FaultyConformancePlugin implements DurableExecutionPlugin { + + private static boolean isStep(String type) { + return "STEP".equals(type); + } + + @Override + public void onInvocationStart(InvocationInfo info) { + System.out.println("CONFPLUGIN faulty invocation-start"); + throw new RuntimeException("faulty invocation-start"); + } + + @Override + public void onInvocationEnd(InvocationEndInfo info) { + System.out.println("CONFPLUGIN faulty invocation-end"); + throw new RuntimeException("faulty invocation-end"); + } + + @Override + public void onOperationStart(OperationInfo info) { + if (isStep(info.type())) { + System.out.println("CONFPLUGIN faulty operation-start"); + throw new RuntimeException("faulty operation-start"); + } + } + + @Override + public void onOperationEnd(OperationEndInfo info) { + if (isStep(info.type())) { + System.out.println("CONFPLUGIN faulty operation-end"); + throw new RuntimeException("faulty operation-end"); + } + } + + @Override + public void onUserFunctionStart(UserFunctionStartInfo info) { + if (isStep(info.type())) { + System.out.println("CONFPLUGIN faulty attempt-start"); + throw new RuntimeException("faulty attempt-start"); + } + } + + @Override + public void onUserFunctionEnd(UserFunctionEndInfo info) { + if (isStep(info.type())) { + System.out.println("CONFPLUGIN faulty attempt-end"); + throw new RuntimeException("faulty attempt-end"); + } + } +} diff --git a/conformance-tests/src/main/java/plugin/PluginAttemptHooksRetry.java b/conformance-tests/src/main/java/plugin/PluginAttemptHooksRetry.java new file mode 100644 index 000000000..d9c1659a9 --- /dev/null +++ b/conformance-tests/src/main/java/plugin/PluginAttemptHooksRetry.java @@ -0,0 +1,50 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import java.time.Duration; +import software.amazon.lambda.durable.DurableConfig; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.config.StepConfig; +import software.amazon.lambda.durable.retry.RetryDecision; + +/** + * 10-3: Plugin attempt hooks fire per step attempt with attempt number and outcome. + * + *

A single step that fails on the first attempt and succeeds on the second (driven by the SDK's built-in + * {@code getAttempt()} and a real retry strategy), configured with {@link ConformanceLoggingPlugin}. The plugin logs + * {@code attempt-start n=} / {@code attempt-end n= outcome=} from the user-function hooks, + * which run on the same thread as the step body so their order is deterministic. + */ +@SuppressWarnings("deprecation") +public class PluginAttemptHooksRetry extends DurableHandler { + + @Override + protected DurableConfig createConfiguration() { + return DurableConfig.builder() + .withPlugins(new ConformanceLoggingPlugin("CONFPLUGIN")) + .build(); + } + + @Override + public String handleRequest(Object input, DurableContext context) { + return context.step( + "retry-step", + String.class, + stepCtx -> { + // Fail on the first attempt, succeed on the second, using the SDK's + // built-in 1-based attempt number. + if (stepCtx.getAttempt() < 2) { + throw new RuntimeException("Attempt " + stepCtx.getAttempt() + " failed"); + } + return "Operation succeeded"; + }, + StepConfig.builder() + .retryStrategy((error, attempt) -> { + if (attempt >= 3) return RetryDecision.fail(); + return RetryDecision.retry(Duration.ofSeconds(1)); + }) + .build()); + } +} diff --git a/conformance-tests/src/main/java/plugin/PluginErrorIsolation.java b/conformance-tests/src/main/java/plugin/PluginErrorIsolation.java new file mode 100644 index 000000000..59aeb80ce --- /dev/null +++ b/conformance-tests/src/main/java/plugin/PluginErrorIsolation.java @@ -0,0 +1,28 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import software.amazon.lambda.durable.DurableConfig; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +/** + * 10-4: Plugin exceptions are swallowed and never affect the execution outcome. + * + *

A single greeting step configured with {@link FaultyConformancePlugin}, whose every hook logs a line and then + * throws. The SDK must catch and ignore every plugin exception so the execution result and history are identical to + * running without the plugin. + */ +@SuppressWarnings("deprecation") +public class PluginErrorIsolation extends DurableHandler { + + @Override + protected DurableConfig createConfiguration() { + return DurableConfig.builder().withPlugins(new FaultyConformancePlugin()).build(); + } + + @Override + public String handleRequest(String input, DurableContext context) { + return context.step("greet", String.class, stepCtx -> "Hello, " + input + "!"); + } +} diff --git a/conformance-tests/src/main/java/plugin/PluginFirstInvocationFlag.java b/conformance-tests/src/main/java/plugin/PluginFirstInvocationFlag.java new file mode 100644 index 000000000..31e136446 --- /dev/null +++ b/conformance-tests/src/main/java/plugin/PluginFirstInvocationFlag.java @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import java.time.Duration; +import software.amazon.lambda.durable.DurableConfig; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +/** + * 10-6: Plugin sees is-first-invocation true once, then false on replay. + * + *

A single 2-second wait configured with {@link ConformanceLoggingPlugin}. The first invocation reports + * {@code first=true} (then suspends), and the replay invocation reports {@code first=false} and finalizes with + * {@code status=SUCCEEDED}. + */ +@SuppressWarnings("deprecation") +public class PluginFirstInvocationFlag extends DurableHandler { + + @Override + protected DurableConfig createConfiguration() { + return DurableConfig.builder() + .withPlugins(new ConformanceLoggingPlugin("CONFPLUGIN")) + .build(); + } + + @Override + public String handleRequest(Object input, DurableContext context) { + context.wait(null, Duration.ofSeconds(2)); + return "Wait completed"; + } +} diff --git a/conformance-tests/src/main/java/plugin/PluginInvocationLifecycle.java b/conformance-tests/src/main/java/plugin/PluginInvocationLifecycle.java new file mode 100644 index 000000000..a407f2674 --- /dev/null +++ b/conformance-tests/src/main/java/plugin/PluginInvocationLifecycle.java @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import software.amazon.lambda.durable.DurableConfig; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +/** + * 10-1: Plugin invocation lifecycle hooks (start and end on a single invocation). + * + *

A single greeting step configured with {@link ConformanceLoggingPlugin}. The plugin's invocation-start hook logs + * {@code first=true} before the handler runs and its invocation-end hook logs {@code status=SUCCEEDED} after the + * result is finalized. The step body logs its running line via the context logger. + */ +@SuppressWarnings("deprecation") +public class PluginInvocationLifecycle extends DurableHandler { + + @Override + protected DurableConfig createConfiguration() { + return DurableConfig.builder() + .withPlugins(new ConformanceLoggingPlugin("CONFPLUGIN")) + .build(); + } + + @Override + public String handleRequest(String input, DurableContext context) { + return context.step("greet", String.class, stepCtx -> { + stepCtx.getLogger().info("Greeting step running for: {}", input); + return "Hello, " + input + "!"; + }); + } +} diff --git a/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java b/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java new file mode 100644 index 000000000..eb33e4075 --- /dev/null +++ b/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import software.amazon.lambda.durable.DurableConfig; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +/** + * 10-5: Multiple registered plugins all receive lifecycle hooks. + * + *

A single greeting step configured with TWO {@link ConformanceLoggingPlugin} instances registered together in + * order A, B. Plugin A logs with prefix {@code CONFPLUGIN-A}, plugin B with prefix {@code CONFPLUGIN-B}. Both plugins + * must receive the invocation-start and invocation-end hooks exactly once. + */ +@SuppressWarnings("deprecation") +public class PluginMultiplePlugins extends DurableHandler { + + @Override + protected DurableConfig createConfiguration() { + return DurableConfig.builder() + .withPlugins( + new ConformanceLoggingPlugin("CONFPLUGIN-A"), + new ConformanceLoggingPlugin("CONFPLUGIN-B")) + .build(); + } + + @Override + public String handleRequest(String input, DurableContext context) { + return context.step("greet", String.class, stepCtx -> "Hello, " + input + "!"); + } +} diff --git a/conformance-tests/src/main/java/plugin/PluginOperationLifecycle.java b/conformance-tests/src/main/java/plugin/PluginOperationLifecycle.java new file mode 100644 index 000000000..ecbb39bb9 --- /dev/null +++ b/conformance-tests/src/main/java/plugin/PluginOperationLifecycle.java @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import software.amazon.lambda.durable.DurableConfig; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +/** + * 10-2: Plugin operation lifecycle hooks (step start and terminal end). + * + *

A single greeting step configured with {@link ConformanceLoggingPlugin}. The plugin, filtering to step-type + * operations, logs {@code operation-start} when the step's STARTED checkpoint is observed and + * {@code operation-end status=SUCCEEDED} when the step reaches its terminal status. + */ +@SuppressWarnings("deprecation") +public class PluginOperationLifecycle extends DurableHandler { + + @Override + protected DurableConfig createConfiguration() { + return DurableConfig.builder() + .withPlugins(new ConformanceLoggingPlugin("CONFPLUGIN")) + .build(); + } + + @Override + public String handleRequest(String input, DurableContext context) { + return context.step("greet", String.class, stepCtx -> "Hello, " + input + "!"); + } +} diff --git a/conformance-tests/src/main/java/plugin/PluginTerminalFailure.java b/conformance-tests/src/main/java/plugin/PluginTerminalFailure.java new file mode 100644 index 000000000..9ba9cf7da --- /dev/null +++ b/conformance-tests/src/main/java/plugin/PluginTerminalFailure.java @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package plugin; + +import software.amazon.lambda.durable.DurableConfig; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.config.StepConfig; +import software.amazon.lambda.durable.retry.RetryStrategies; + +/** + * 10-7: Plugin invocation-end hook receives FAILED status when execution fails. + * + *

A single step that always throws, configured with no retries and {@link ConformanceLoggingPlugin}. The plugin + * logs {@code invocation-start first=true}; the step throws, no retry is attempted, and the plugin's invocation-end + * hook fires with {@code status=FAILED}. + */ +@SuppressWarnings("deprecation") +public class PluginTerminalFailure extends DurableHandler { + + @Override + protected DurableConfig createConfiguration() { + return DurableConfig.builder() + .withPlugins(new ConformanceLoggingPlugin("CONFPLUGIN")) + .build(); + } + + @Override + public String handleRequest(Object input, DurableContext context) { + return context.step( + "failing-step", + String.class, + stepCtx -> { + throw new RuntimeException("Something went wrong"); + }, + StepConfig.builder() + .retryStrategy(RetryStrategies.Presets.NO_RETRY) + .build()); + } +} diff --git a/conformance-tests/template_plugin.yaml b/conformance-tests/template_plugin.yaml new file mode 100644 index 000000000..a1b690b15 --- /dev/null +++ b/conformance-tests/template_plugin.yaml @@ -0,0 +1,166 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Durable Execution Conformance Test Examples - Java (Plugin) + +Parameters: + Architecture: + Type: String + Default: arm64 + Description: Lambda Function Architecture + AllowedValues: + - x86_64 + - arm64 + JavaVersion: + Type: String + Default: 'java21' + Description: Java runtime version + +Globals: + Function: + Timeout: 60 + MemorySize: 512 + Runtime: + Ref: JavaVersion + Architectures: + - Ref: Architecture + +Resources: + DurableFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: DurableExecutionPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:CheckpointDurableExecution + - lambda:GetDurableExecutionState + Resource: '*' + + PluginInvocationLifecycle: + Type: AWS::Serverless::Function + TestingMetadata: + TestDescription: ["10-1"] + Properties: + CodeUri: . + Handler: plugin.PluginInvocationLifecycle + Description: Plugin invocation lifecycle hooks (start and end on a single invocation) + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + Environment: + Variables: + # The step body logs its running line via the context logger (Log4j2), + # whose default level is ERROR and would suppress logger.info. Raise to INFO. + JAVA_TOOL_OPTIONS: -Dorg.apache.logging.log4j.level=INFO + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + + PluginOperationLifecycle: + Type: AWS::Serverless::Function + TestingMetadata: + TestDescription: ["10-2"] + Properties: + CodeUri: . + Handler: plugin.PluginOperationLifecycle + Description: Plugin operation lifecycle hooks (step start and terminal end) + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + + PluginAttemptHooksRetry: + Type: AWS::Serverless::Function + TestingMetadata: + TestDescription: ["10-3"] + Properties: + CodeUri: . + Handler: plugin.PluginAttemptHooksRetry + Description: Plugin attempt hooks fire per step attempt with attempt number and outcome + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + + PluginErrorIsolation: + Type: AWS::Serverless::Function + TestingMetadata: + TestDescription: ["10-4"] + Properties: + CodeUri: . + Handler: plugin.PluginErrorIsolation + Description: Plugin exceptions are swallowed and never affect the execution outcome + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + + PluginMultiplePlugins: + Type: AWS::Serverless::Function + TestingMetadata: + TestDescription: ["10-5"] + Properties: + CodeUri: . + Handler: plugin.PluginMultiplePlugins + Description: Multiple registered plugins all receive lifecycle hooks + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + + PluginFirstInvocationFlag: + Type: AWS::Serverless::Function + TestingMetadata: + TestDescription: ["10-6"] + Properties: + CodeUri: . + Handler: plugin.PluginFirstInvocationFlag + Description: Plugin sees is-first-invocation true once, then false on replay + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 + + PluginTerminalFailure: + Type: AWS::Serverless::Function + TestingMetadata: + TestDescription: ["10-7"] + Properties: + CodeUri: . + Handler: plugin.PluginTerminalFailure + Description: Plugin invocation-end hook receives FAILED status when execution fails + Role: + Fn::GetAtt: + - DurableFunctionRole + - Arn + DurableConfig: + RetentionPeriodInDays: 7 + ExecutionTimeout: 300 From f740aa5e6d04a1edbad09603d6dc4a99a359cc43 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Fri, 24 Jul 2026 22:29:28 +0000 Subject: [PATCH 2/7] Emit hook-received operation id in plugin conformance log lines (10-2, 10-3) --- .../src/main/java/plugin/ConformanceLoggingPlugin.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java b/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java index bcc0c5420..e7aa88438 100644 --- a/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java +++ b/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java @@ -44,21 +44,21 @@ public void onInvocationEnd(InvocationEndInfo info) { @Override public void onOperationStart(OperationInfo info) { if (isStep(info.type())) { - System.out.println(prefix + " operation-start"); + System.out.println(prefix + " operation-start op=" + info.id()); } } @Override public void onOperationEnd(OperationEndInfo info) { if (isStep(info.type())) { - System.out.println(prefix + " operation-end status=" + info.status()); + System.out.println(prefix + " operation-end op=" + info.id() + " status=" + info.status()); } } @Override public void onUserFunctionStart(UserFunctionStartInfo info) { if (isStep(info.type()) && info.attempt() != null) { - System.out.println(prefix + " attempt-start n=" + info.attempt()); + System.out.println(prefix + " attempt-start n=" + info.attempt() + " op=" + info.id()); } } @@ -66,7 +66,8 @@ public void onUserFunctionStart(UserFunctionStartInfo info) { public void onUserFunctionEnd(UserFunctionEndInfo info) { if (isStep(info.type()) && info.attempt() != null) { String outcome = info.succeeded() ? "SUCCEEDED" : "FAILED"; - System.out.println(prefix + " attempt-end n=" + info.attempt() + " outcome=" + outcome); + System.out.println( + prefix + " attempt-end n=" + info.attempt() + " outcome=" + outcome + " op=" + info.id()); } } } From 2499419814a92b10b4100ee02336ca6ca835306c Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Sat, 25 Jul 2026 00:39:09 +0000 Subject: [PATCH 3/7] Emit exactly the 10-5 documented lines (dedicated invocation plugin) The exact-match ExpectedLogs schema exposed that this handler reused the general ConformanceLoggingPlugin, emitting 'CONFPLUGIN-A invocation-start first=true' where the requirement documents 'CONFPLUGIN-A invocation-start' (as JS/Python emit). Use a minimal purpose-built plugin matching the requirement byte-for-byte. --- .../java/plugin/PluginMultiplePlugins.java | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java b/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java index eb33e4075..7313c7ab6 100644 --- a/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java +++ b/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java @@ -5,23 +5,44 @@ import software.amazon.lambda.durable.DurableConfig; import software.amazon.lambda.durable.DurableContext; import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.plugin.DurableExecutionPlugin; +import software.amazon.lambda.durable.plugin.InvocationEndInfo; +import software.amazon.lambda.durable.plugin.InvocationInfo; /** * 10-5: Multiple registered plugins all receive lifecycle hooks. * - *

A single greeting step configured with TWO {@link ConformanceLoggingPlugin} instances registered together in - * order A, B. Plugin A logs with prefix {@code CONFPLUGIN-A}, plugin B with prefix {@code CONFPLUGIN-B}. Both plugins - * must receive the invocation-start and invocation-end hooks exactly once. + *

A single greeting step configured with TWO invocation-logging plugins registered together in order A, B. Plugin A + * logs with prefix {@code CONFPLUGIN-A}, plugin B with prefix {@code CONFPLUGIN-B}, emitting exactly the lines the + * requirement documents: {@code invocation-start} and {@code invocation-end status=}. */ @SuppressWarnings("deprecation") public class PluginMultiplePlugins extends DurableHandler { + /** Minimal plugin emitting exactly the 10-5 documented invocation lines. */ + static final class InvocationLoggingPlugin implements DurableExecutionPlugin { + private final String prefix; + + InvocationLoggingPlugin(String prefix) { + this.prefix = prefix; + } + + @Override + public void onInvocationStart(InvocationInfo info) { + System.out.println(prefix + " invocation-start"); + } + + @Override + public void onInvocationEnd(InvocationEndInfo info) { + System.out.println(prefix + " invocation-end status=" + info.invocationStatus().name()); + } + } + @Override protected DurableConfig createConfiguration() { return DurableConfig.builder() .withPlugins( - new ConformanceLoggingPlugin("CONFPLUGIN-A"), - new ConformanceLoggingPlugin("CONFPLUGIN-B")) + new InvocationLoggingPlugin("CONFPLUGIN-A"), new InvocationLoggingPlugin("CONFPLUGIN-B")) .build(); } From dccb0ae30cb32bda9b439ce9adaf59c5c6e1ce29 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Mon, 27 Jul 2026 18:27:40 +0000 Subject: [PATCH 4/7] Emit structured JSON records from plugin hooks One JSON object per hook ({plugin, hook, first|status|n|outcome|op}), values from the real hook info parameters; replaces packed text lines so specs assert typed fields directly. --- .../java/plugin/ConformanceLoggingPlugin.java | 24 +++++++++++++------ .../java/plugin/FaultyConformancePlugin.java | 12 +++++----- .../java/plugin/PluginMultiplePlugins.java | 7 ++++-- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java b/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java index e7aa88438..17d01203b 100644 --- a/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java +++ b/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java @@ -33,32 +33,41 @@ private static boolean isStep(String type) { @Override public void onInvocationStart(InvocationInfo info) { - System.out.println(prefix + " invocation-start first=" + info.isFirstInvocation()); + System.out.println(String.format( + "{\"plugin\": \"%s\", \"hook\": \"invocation-start\", \"first\": %b}", + prefix, info.isFirstInvocation())); } @Override public void onInvocationEnd(InvocationEndInfo info) { - System.out.println(prefix + " invocation-end status=" + info.invocationStatus().name()); + System.out.println(String.format( + "{\"plugin\": \"%s\", \"hook\": \"invocation-end\", \"status\": \"%s\"}", + prefix, info.invocationStatus().name())); } @Override public void onOperationStart(OperationInfo info) { if (isStep(info.type())) { - System.out.println(prefix + " operation-start op=" + info.id()); + System.out.println(String.format( + "{\"plugin\": \"%s\", \"hook\": \"operation-start\", \"op\": \"%s\"}", prefix, info.id())); } } @Override public void onOperationEnd(OperationEndInfo info) { if (isStep(info.type())) { - System.out.println(prefix + " operation-end op=" + info.id() + " status=" + info.status()); + System.out.println(String.format( + "{\"plugin\": \"%s\", \"hook\": \"operation-end\", \"op\": \"%s\", \"status\": \"%s\"}", + prefix, info.id(), info.status())); } } @Override public void onUserFunctionStart(UserFunctionStartInfo info) { if (isStep(info.type()) && info.attempt() != null) { - System.out.println(prefix + " attempt-start n=" + info.attempt() + " op=" + info.id()); + System.out.println(String.format( + "{\"plugin\": \"%s\", \"hook\": \"attempt-start\", \"n\": %d, \"op\": \"%s\"}", + prefix, info.attempt(), info.id())); } } @@ -66,8 +75,9 @@ public void onUserFunctionStart(UserFunctionStartInfo info) { public void onUserFunctionEnd(UserFunctionEndInfo info) { if (isStep(info.type()) && info.attempt() != null) { String outcome = info.succeeded() ? "SUCCEEDED" : "FAILED"; - System.out.println( - prefix + " attempt-end n=" + info.attempt() + " outcome=" + outcome + " op=" + info.id()); + System.out.println(String.format( + "{\"plugin\": \"%s\", \"hook\": \"attempt-end\", \"n\": %d, \"outcome\": \"%s\", \"op\": \"%s\"}", + prefix, info.attempt(), outcome, info.id())); } } } diff --git a/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java b/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java index baf5c96c7..c59f2cdb7 100644 --- a/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java +++ b/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java @@ -26,20 +26,20 @@ private static boolean isStep(String type) { @Override public void onInvocationStart(InvocationInfo info) { - System.out.println("CONFPLUGIN faulty invocation-start"); + System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-start\"}"); throw new RuntimeException("faulty invocation-start"); } @Override public void onInvocationEnd(InvocationEndInfo info) { - System.out.println("CONFPLUGIN faulty invocation-end"); + System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-end\"}"); throw new RuntimeException("faulty invocation-end"); } @Override public void onOperationStart(OperationInfo info) { if (isStep(info.type())) { - System.out.println("CONFPLUGIN faulty operation-start"); + System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-start\"}"); throw new RuntimeException("faulty operation-start"); } } @@ -47,7 +47,7 @@ public void onOperationStart(OperationInfo info) { @Override public void onOperationEnd(OperationEndInfo info) { if (isStep(info.type())) { - System.out.println("CONFPLUGIN faulty operation-end"); + System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-end\"}"); throw new RuntimeException("faulty operation-end"); } } @@ -55,7 +55,7 @@ public void onOperationEnd(OperationEndInfo info) { @Override public void onUserFunctionStart(UserFunctionStartInfo info) { if (isStep(info.type())) { - System.out.println("CONFPLUGIN faulty attempt-start"); + System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-start\"}"); throw new RuntimeException("faulty attempt-start"); } } @@ -63,7 +63,7 @@ public void onUserFunctionStart(UserFunctionStartInfo info) { @Override public void onUserFunctionEnd(UserFunctionEndInfo info) { if (isStep(info.type())) { - System.out.println("CONFPLUGIN faulty attempt-end"); + System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-end\"}"); throw new RuntimeException("faulty attempt-end"); } } diff --git a/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java b/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java index 7313c7ab6..42ffb048b 100644 --- a/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java +++ b/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java @@ -29,12 +29,15 @@ static final class InvocationLoggingPlugin implements DurableExecutionPlugin { @Override public void onInvocationStart(InvocationInfo info) { - System.out.println(prefix + " invocation-start"); + System.out.println( + String.format("{\"plugin\": \"%s\", \"hook\": \"invocation-start\"}", prefix)); } @Override public void onInvocationEnd(InvocationEndInfo info) { - System.out.println(prefix + " invocation-end status=" + info.invocationStatus().name()); + System.out.println(String.format( + "{\"plugin\": \"%s\", \"hook\": \"invocation-end\", \"status\": \"%s\"}", + prefix, info.invocationStatus().name())); } } From 3694534e9f0145af0e64c85577607a1c2b9cc03c Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Mon, 27 Jul 2026 20:49:26 +0000 Subject: [PATCH 5/7] Stamp durableExecutionArn on plugin records Records carry the execution ARN received via the invocation-start hook (captured in a volatile field for operation/user-function hooks) so the runner's execution-scoped CloudWatch JSON filter retrieves them. --- .../java/plugin/ConformanceLoggingPlugin.java | 32 ++++++++++++------- .../java/plugin/FaultyConformancePlugin.java | 27 ++++++++++++---- .../java/plugin/PluginMultiplePlugins.java | 17 +++++++--- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java b/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java index 17d01203b..0315128de 100644 --- a/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java +++ b/conformance-tests/src/main/java/plugin/ConformanceLoggingPlugin.java @@ -23,6 +23,9 @@ public class ConformanceLoggingPlugin implements DurableExecutionPlugin { private final String prefix; + /** Captured from onInvocationStart; read by later hooks that may run on other threads. */ + private volatile String executionArn; + public ConformanceLoggingPlugin(String prefix) { this.prefix = prefix; } @@ -31,25 +34,32 @@ private static boolean isStep(String type) { return "STEP".equals(type); } + /** Returns {@code , "durableExecutionArn": ""} when captured, otherwise an empty string. */ + private String arnField() { + return executionArn == null ? "" : String.format(", \"durableExecutionArn\": \"%s\"", executionArn); + } + @Override public void onInvocationStart(InvocationInfo info) { + this.executionArn = info.durableExecutionArn(); System.out.println(String.format( - "{\"plugin\": \"%s\", \"hook\": \"invocation-start\", \"first\": %b}", - prefix, info.isFirstInvocation())); + "{\"plugin\": \"%s\", \"hook\": \"invocation-start\", \"first\": %b%s}", + prefix, info.isFirstInvocation(), arnField())); } @Override public void onInvocationEnd(InvocationEndInfo info) { System.out.println(String.format( - "{\"plugin\": \"%s\", \"hook\": \"invocation-end\", \"status\": \"%s\"}", - prefix, info.invocationStatus().name())); + "{\"plugin\": \"%s\", \"hook\": \"invocation-end\", \"status\": \"%s\"%s}", + prefix, info.invocationStatus().name(), arnField())); } @Override public void onOperationStart(OperationInfo info) { if (isStep(info.type())) { System.out.println(String.format( - "{\"plugin\": \"%s\", \"hook\": \"operation-start\", \"op\": \"%s\"}", prefix, info.id())); + "{\"plugin\": \"%s\", \"hook\": \"operation-start\", \"op\": \"%s\"%s}", + prefix, info.id(), arnField())); } } @@ -57,8 +67,8 @@ public void onOperationStart(OperationInfo info) { public void onOperationEnd(OperationEndInfo info) { if (isStep(info.type())) { System.out.println(String.format( - "{\"plugin\": \"%s\", \"hook\": \"operation-end\", \"op\": \"%s\", \"status\": \"%s\"}", - prefix, info.id(), info.status())); + "{\"plugin\": \"%s\", \"hook\": \"operation-end\", \"op\": \"%s\", \"status\": \"%s\"%s}", + prefix, info.id(), info.status(), arnField())); } } @@ -66,8 +76,8 @@ public void onOperationEnd(OperationEndInfo info) { public void onUserFunctionStart(UserFunctionStartInfo info) { if (isStep(info.type()) && info.attempt() != null) { System.out.println(String.format( - "{\"plugin\": \"%s\", \"hook\": \"attempt-start\", \"n\": %d, \"op\": \"%s\"}", - prefix, info.attempt(), info.id())); + "{\"plugin\": \"%s\", \"hook\": \"attempt-start\", \"n\": %d, \"op\": \"%s\"%s}", + prefix, info.attempt(), info.id(), arnField())); } } @@ -76,8 +86,8 @@ public void onUserFunctionEnd(UserFunctionEndInfo info) { if (isStep(info.type()) && info.attempt() != null) { String outcome = info.succeeded() ? "SUCCEEDED" : "FAILED"; System.out.println(String.format( - "{\"plugin\": \"%s\", \"hook\": \"attempt-end\", \"n\": %d, \"outcome\": \"%s\", \"op\": \"%s\"}", - prefix, info.attempt(), outcome, info.id())); + "{\"plugin\": \"%s\", \"hook\": \"attempt-end\", \"n\": %d, \"outcome\": \"%s\", \"op\": \"%s\"%s}", + prefix, info.attempt(), outcome, info.id(), arnField())); } } } diff --git a/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java b/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java index c59f2cdb7..2b5e2306c 100644 --- a/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java +++ b/conformance-tests/src/main/java/plugin/FaultyConformancePlugin.java @@ -24,22 +24,34 @@ private static boolean isStep(String type) { return "STEP".equals(type); } + /** Captured from onInvocationStart (before the throw); read by later hooks that may run on other threads. */ + private volatile String executionArn; + + /** Returns {@code , "durableExecutionArn": ""} when captured, otherwise an empty string. */ + private String arnField() { + return executionArn == null ? "" : String.format(", \"durableExecutionArn\": \"%s\"", executionArn); + } + @Override public void onInvocationStart(InvocationInfo info) { - System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-start\"}"); + this.executionArn = info.durableExecutionArn(); + System.out.println(String.format( + "{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-start\"%s}", arnField())); throw new RuntimeException("faulty invocation-start"); } @Override public void onInvocationEnd(InvocationEndInfo info) { - System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-end\"}"); + System.out.println(String.format( + "{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"invocation-end\"%s}", arnField())); throw new RuntimeException("faulty invocation-end"); } @Override public void onOperationStart(OperationInfo info) { if (isStep(info.type())) { - System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-start\"}"); + System.out.println(String.format( + "{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-start\"%s}", arnField())); throw new RuntimeException("faulty operation-start"); } } @@ -47,7 +59,8 @@ public void onOperationStart(OperationInfo info) { @Override public void onOperationEnd(OperationEndInfo info) { if (isStep(info.type())) { - System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-end\"}"); + System.out.println(String.format( + "{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"operation-end\"%s}", arnField())); throw new RuntimeException("faulty operation-end"); } } @@ -55,7 +68,8 @@ public void onOperationEnd(OperationEndInfo info) { @Override public void onUserFunctionStart(UserFunctionStartInfo info) { if (isStep(info.type())) { - System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-start\"}"); + System.out.println(String.format( + "{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-start\"%s}", arnField())); throw new RuntimeException("faulty attempt-start"); } } @@ -63,7 +77,8 @@ public void onUserFunctionStart(UserFunctionStartInfo info) { @Override public void onUserFunctionEnd(UserFunctionEndInfo info) { if (isStep(info.type())) { - System.out.println("{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-end\"}"); + System.out.println(String.format( + "{\"plugin\": \"CONFPLUGIN-FAULTY\", \"hook\": \"attempt-end\"%s}", arnField())); throw new RuntimeException("faulty attempt-end"); } } diff --git a/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java b/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java index 42ffb048b..4dde9f6b3 100644 --- a/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java +++ b/conformance-tests/src/main/java/plugin/PluginMultiplePlugins.java @@ -23,21 +23,30 @@ public class PluginMultiplePlugins extends DurableHandler { static final class InvocationLoggingPlugin implements DurableExecutionPlugin { private final String prefix; + /** Captured from onInvocationStart; read by onInvocationEnd which may run on another thread. */ + private volatile String executionArn; + InvocationLoggingPlugin(String prefix) { this.prefix = prefix; } + /** Returns {@code , "durableExecutionArn": ""} when captured, otherwise an empty string. */ + private String arnField() { + return executionArn == null ? "" : String.format(", \"durableExecutionArn\": \"%s\"", executionArn); + } + @Override public void onInvocationStart(InvocationInfo info) { - System.out.println( - String.format("{\"plugin\": \"%s\", \"hook\": \"invocation-start\"}", prefix)); + this.executionArn = info.durableExecutionArn(); + System.out.println(String.format( + "{\"plugin\": \"%s\", \"hook\": \"invocation-start\"%s}", prefix, arnField())); } @Override public void onInvocationEnd(InvocationEndInfo info) { System.out.println(String.format( - "{\"plugin\": \"%s\", \"hook\": \"invocation-end\", \"status\": \"%s\"}", - prefix, info.invocationStatus().name())); + "{\"plugin\": \"%s\", \"hook\": \"invocation-end\", \"status\": \"%s\"%s}", + prefix, info.invocationStatus().name(), arnField())); } } From 46960e19b31fc8ebd37759c52bc1a916ffb1dcab Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Mon, 27 Jul 2026 21:34:40 +0000 Subject: [PATCH 6/7] Enable JSON log format for plugin suite functions Adopt upstream's LoggingConfig LogFormat JSON + log4j2.xml (Lambda appender with JsonTemplateLayout resolving MDC executionArn into durableExecutionArn) so SDK-logger lines are execution-scoped retrievable; drop the obsolete JAVA_TOOL_OPTIONS level hack. --- conformance-tests/template_plugin.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/conformance-tests/template_plugin.yaml b/conformance-tests/template_plugin.yaml index a1b690b15..902223ddd 100644 --- a/conformance-tests/template_plugin.yaml +++ b/conformance-tests/template_plugin.yaml @@ -23,6 +23,8 @@ Globals: Ref: JavaVersion Architectures: - Ref: Architecture + LoggingConfig: + LogFormat: JSON Resources: DurableFunctionRole: @@ -60,11 +62,6 @@ Resources: Fn::GetAtt: - DurableFunctionRole - Arn - Environment: - Variables: - # The step body logs its running line via the context logger (Log4j2), - # whose default level is ERROR and would suppress logger.info. Raise to INFO. - JAVA_TOOL_OPTIONS: -Dorg.apache.logging.log4j.level=INFO DurableConfig: RetentionPeriodInDays: 7 ExecutionTimeout: 300 From bda11b76e34af9a0b07fbc53e46f2132bd7cc538 Mon Sep 17 00:00:00 2001 From: Alex Wang Date: Mon, 27 Jul 2026 23:02:48 +0000 Subject: [PATCH 7/7] ci: discover conformance suites from templates instead of a hardcoded matrix Mirrors the Python repo: a discover_suites job runs conformance-tests/scripts/discover_suites.py (validating each template_.yaml has a non-empty src/main/java// package) and feeds strategy.matrix.suite, so a new suite runs in CI as soon as its template + handlers ship. --- .github/workflows/conformance-tests.yml | 33 ++++++++------ conformance-tests/scripts/discover_suites.py | 47 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 conformance-tests/scripts/discover_suites.py diff --git a/.github/workflows/conformance-tests.yml b/.github/workflows/conformance-tests.yml index 93dad53ca..557f01d02 100644 --- a/.github/workflows/conformance-tests.yml +++ b/.github/workflows/conformance-tests.yml @@ -2,9 +2,10 @@ name: Conformance Tests # Full-integration conformance run: builds the SDK + Java handlers, installs the # pinned language-agnostic runner from PyPI, then deploys + invokes + validates -# one SAM stack per suite. Each suite runs as its own parallel matrix job. To add -# a suite later, add its name to `strategy.matrix.suite` (and ship its -# template_.yaml + handlers under conformance-tests/). +# one SAM stack per suite. Suites are discovered from the module's +# template_.yaml files (see conformance-tests/scripts/discover_suites.py) +# and each runs as its own parallel matrix job, so adding a suite only requires +# shipping its template + handlers under conformance-tests/. on: pull_request: @@ -28,24 +29,30 @@ permissions: id-token: write # Required for AWS OIDC credentials jobs: + discover_suites: + name: discover conformance suites + runs-on: ubuntu-latest + outputs: + suites: ${{ steps.discover.outputs.suites }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Discover suites from templates + id: discover + working-directory: conformance-tests + run: echo "suites=$(python3 scripts/discover_suites.py)" >> "$GITHUB_OUTPUT" + conformance: name: conformance (${{ matrix.suite }}) + needs: discover_suites runs-on: ubuntu-latest env: AWS_REGION: us-west-2 strategy: fail-fast: false matrix: - suite: - - step - - wait - - child - - callback - - invoke - - parallel - - wait_for_callback - - wait_for_condition - - map + suite: ${{ fromJSON(needs.discover_suites.outputs.suites) }} defaults: run: working-directory: conformance-tests diff --git a/conformance-tests/scripts/discover_suites.py b/conformance-tests/scripts/discover_suites.py new file mode 100644 index 000000000..8a307f0b9 --- /dev/null +++ b/conformance-tests/scripts/discover_suites.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Discover conformance suites from template files.""" + +from __future__ import annotations + +import json +from pathlib import Path + + +MODULE_DIR = Path(__file__).resolve().parents[1] +TEMPLATE_PREFIX = "template_" +TEMPLATE_SUFFIX = ".yaml" +SOURCE_ROOT = Path("src") / "main" / "java" + + +def discover_suites(module_dir: Path = MODULE_DIR) -> tuple[str, ...]: + """Return sorted suites with matching templates and non-empty handler packages.""" + templates = sorted(module_dir.glob(f"{TEMPLATE_PREFIX}*{TEMPLATE_SUFFIX}")) + if not templates: + raise SystemExit(f"No {TEMPLATE_PREFIX}{TEMPLATE_SUFFIX} files found") + + suites: list[str] = [] + for template in templates: + suite = template.name[len(TEMPLATE_PREFIX) : -len(TEMPLATE_SUFFIX)] + if not suite: + raise SystemExit(f"Invalid conformance template name: {template.name}") + + handlers_dir = module_dir / SOURCE_ROOT / suite + if not handlers_dir.is_dir(): + raise SystemExit( + f"Template {template.name} has no matching handler package: {handlers_dir}" + ) + + if not list(handlers_dir.glob("*.java")): + raise SystemExit(f"No handler classes found for suite {suite}: {handlers_dir}") + + suites.append(suite) + + return tuple(suites) + + +def main() -> None: + print(json.dumps(discover_suites(), separators=(",", ":"))) + + +if __name__ == "__main__": + main()