From 95bbfdbe70dea6c38a83b8c02f5eac3ba91210df Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 9 Feb 2024 08:22:23 +0000 Subject: [PATCH] Test all combinations of create-aws-lambda cli command (#2316) * Test all combinations of create-aws-lambda cli command And test them all when compiled natively. * Test invalid paths as well Previously we skipped testing these which is bad if something changes unexpectedly --- .../command/CreateLambdaBuilderCommand.java | 12 +- .../CreateLambdaBuilderCommandSpec.groovy | 240 +++++++++++------- .../CreateLambdaCommandCliOptions.java | 122 +++++++++ .../cli/command/CreateLambdaCommandTest.java | 130 ++++++++++ 4 files changed, 409 insertions(+), 95 deletions(-) create mode 100644 test-suite-graal/src/test/java/io/micronaut/starter/cli/command/CreateLambdaCommandCliOptions.java create mode 100644 test-suite-graal/src/test/java/io/micronaut/starter/cli/command/CreateLambdaCommandTest.java diff --git a/starter-cli/src/main/java/io/micronaut/starter/cli/command/CreateLambdaBuilderCommand.java b/starter-cli/src/main/java/io/micronaut/starter/cli/command/CreateLambdaBuilderCommand.java index cc626468a8b..e0ef8eeb821 100644 --- a/starter-cli/src/main/java/io/micronaut/starter/cli/command/CreateLambdaBuilderCommand.java +++ b/starter-cli/src/main/java/io/micronaut/starter/cli/command/CreateLambdaBuilderCommand.java @@ -119,7 +119,7 @@ protected JdkVersion getJdkVersion(LambdaDeployment deployment, LineReader reade return versions[choice]; } - JdkVersion[] jdkVersionsForDeployment(LambdaDeployment deployment) { + static JdkVersion[] jdkVersionsForDeployment(LambdaDeployment deployment) { switch (deployment) { case NATIVE_EXECUTABLE: return new JdkVersion[]{ @@ -145,7 +145,7 @@ protected ApplicationType applicationTypeForCodingStyle(CodingStyle codingStyle) } } - protected Language[] languagesForDeployment(LambdaDeployment deployment) { + static Language[] languagesForDeployment(LambdaDeployment deployment) { return deployment == LambdaDeployment.NATIVE_EXECUTABLE ? Stream.of(Language.values()) .filter(GraalVMFeatureValidator::supports) @@ -229,15 +229,15 @@ protected Optional getCdk(LineReader reader) { : Optional.empty(); } - protected List apiTriggerFeatures(ApplicationType applicationType, Collection features) { + static List apiTriggerFeatures(ApplicationType applicationType, Collection features) { return features.stream() .filter(AwsApiFeature.class::isInstance) .filter(f -> f.supports(applicationType)) .sorted(Comparator.comparing(Feature::getTitle).reversed()) - .collect(Collectors.toList()); + .toList(); } - protected List triggerFeatures(Collection features) { + static List triggerFeatures(Collection features) { return features.stream() .filter(LambdaTrigger.class::isInstance) .sorted((o1, o2) -> { @@ -252,6 +252,6 @@ protected List triggerFeatures(Collection features) { } return o1.getTitle().compareTo(o2.getTitle()); }) - .collect(Collectors.toList()); + .toList(); } } diff --git a/starter-cli/src/test/groovy/io/micronaut/starter/cli/command/CreateLambdaBuilderCommandSpec.groovy b/starter-cli/src/test/groovy/io/micronaut/starter/cli/command/CreateLambdaBuilderCommandSpec.groovy index d64736b4679..46b9d6e8a61 100644 --- a/starter-cli/src/test/groovy/io/micronaut/starter/cli/command/CreateLambdaBuilderCommandSpec.groovy +++ b/starter-cli/src/test/groovy/io/micronaut/starter/cli/command/CreateLambdaBuilderCommandSpec.groovy @@ -3,36 +3,55 @@ package io.micronaut.starter.cli.command import io.micronaut.context.ApplicationContext import io.micronaut.starter.application.ApplicationType import io.micronaut.starter.feature.Feature -import io.micronaut.starter.feature.aws.LambdaTrigger -import spock.lang.Specification +import io.micronaut.starter.feature.architecture.Arm +import io.micronaut.starter.feature.architecture.CpuArchitecture +import io.micronaut.starter.feature.architecture.X86 +import io.micronaut.starter.feature.aws.Cdk +import io.micronaut.starter.feature.function.awslambda.AwsLambda +import io.micronaut.starter.feature.graalvm.GraalVM +import io.micronaut.starter.options.BuildTool +import io.micronaut.starter.options.JdkVersion +import io.micronaut.starter.options.Language +import io.micronaut.starter.options.TestFramework import org.jline.reader.LineReader - -import java.util.stream.Collectors +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification class CreateLambdaBuilderCommandSpec extends Specification { + @Shared + @AutoCleanup + ApplicationContext applicationContext + + @Shared + CreateLambdaBuilderCommand command + + def setupSpec() { + applicationContext = ApplicationContext.run(); + command = applicationContext.getBean(CreateLambdaBuilderCommand) + } + void "test lambda triggers"() { given: ApplicationContext applicationContext = ApplicationContext.run(); CreateLambdaBuilderCommand command = applicationContext.getBean(CreateLambdaBuilderCommand) - Collection features = applicationContext.getBeansOfType(LambdaTrigger) + List features = applicationContext.getBeansOfType(Feature) when: - List result = command.triggerFeatures(features) - .stream() - .map(f -> f.getName()) - .collect(Collectors.toList()); + List result = command.triggerFeatures(features).name + then: - [ + result == [ 'amazon-api-gateway', 'amazon-api-gateway-http', 'aws-lambda-function-url', 'aws-lambda-scheduled-event', 'aws-lambda-s3-event-notification', - ] == result + ] and: - ['amazon-api-gateway', 'amazon-api-gateway-http'] == command.apiTriggerFeatures(command.applicationTypeForCodingStyle(CodingStyle.CONTROLLERS), features)*.name + command.apiTriggerFeatures(command.applicationTypeForCodingStyle(CodingStyle.CONTROLLERS), features).name == ['amazon-api-gateway', 'amazon-api-gateway-http'] cleanup: applicationContext.close() @@ -79,92 +98,135 @@ class CreateLambdaBuilderCommandSpec extends Specification { *1) 11 2) 8 */ - void "test prompt"(List answers, ApplicationType expectedApplicationType, Set expectedFeatures) { - given: - ApplicationContext applicationContext = ApplicationContext.run(); + void "#cliOptions.cliCommands -- #cliOptions"(LambdaCliOptions cliOptions) { + when: CreateLambdaBuilderCommand command = applicationContext.getBean(CreateLambdaBuilderCommand) - when: def reader = Stub(LineReader) { - readLine(BuilderCommand.PROMPT.get()) >>> answers - + readLine(BuilderCommand.PROMPT.get()) >>> cliOptions.cliCommands } GenerateOptions options = command.createGenerateOptions(reader) then: - expectedApplicationType == options.applicationType - expectedFeatures =~ options.features - - cleanup: - applicationContext.close() + cliOptions.expectedApplicationType == options.applicationType + cliOptions.features ==~ options.features + cliOptions.language == options.options.language + cliOptions.buildTool == options.options.buildTool + cliOptions.testFramework == options.options.testFramework + cliOptions.javaVersion == options.options.javaVersion where: - answers | expectedApplicationType | expectedFeatures - [ - "1", // with Controller - "1", // Amazon Api Gateway - "1", // FAT JAR - "1", // ARM - "1", // CDK Yes - "1", // Java - "1", // Junit - "1", // Gradle Groovy DSL - "1", // Java 11 - ] | ApplicationType.DEFAULT | ['amazon-api-gateway', 'arm', 'aws-cdk','aws-lambda'] - [ - "2", // with Controller - "1", // Amazon Api Gateway - "1", // FAT JAR - "1", // ARM - "1", // CDK Yes - "1", // Java - "1", // Junit - "1", // Gradle Groovy DSL - "1", // Java 11 - ] | ApplicationType.FUNCTION | ['amazon-api-gateway', 'arm', 'aws-cdk','aws-lambda'] - [ - "2", // with Controller - "4", // Scheduled event - "1", // FAT JAR - "1", // ARM - "1", // CDK Yes - "1", // Java - "1", // Junit - "1", // Gradle Groovy DSL - "1", // Java 11 - ] | ApplicationType.FUNCTION | ['aws-lambda-scheduled-event', 'arm', 'aws-cdk','aws-lambda'] - [ - "1", // with Controller - "1", // Amazon Api Gateway - "2", // FAT JAR - "1", // ARM - "1", // CDK Yes - "1", // Java - "1", // Junit - "1", // Gradle Groovy DSL - "1", // Java 11 - ] | ApplicationType.DEFAULT | ['amazon-api-gateway', 'arm', 'aws-cdk','aws-lambda','graalvm'] - [ - "1", // with Controller - "1", // Amazon Api Gateway - "1", // FAT JAR - "2", // x86 - "1", // CDK Yes - "1", // Java - "1", // Junit - "1", // Gradle Groovy DSL - "1", // Java 11 - ] | ApplicationType.DEFAULT | ['amazon-api-gateway', 'x86', 'aws-cdk','aws-lambda'] + cliOptions << [CodingStyle.values(), LambdaDeployment.values()].combinations().collectMany { CodingStyle codingStyle, LambdaDeployment deployment -> + combinations( + applicationContext, + codingStyle == CodingStyle.CONTROLLERS ? + CreateLambdaBuilderCommand.apiTriggerFeatures(ApplicationType.DEFAULT, applicationContext.getBeansOfType(Feature)) : + CreateLambdaBuilderCommand.triggerFeatures(applicationContext.getBeansOfType(Feature)), + codingStyle, + deployment + ) + } + } + + static combinations(ApplicationContext ctx, List triggerFeatures, CodingStyle codingStyle, LambdaDeployment deployment) { [ - "1", // with Controller - "1", // Amazon Api Gateway - "1", // FAT JAR - "1", // ARM - "2", // CDK Yes - "1", // Java - "1", // Junit - "1", // Gradle Groovy DSL - "1", // Java 11 - ] | ApplicationType.DEFAULT | ['amazon-api-gateway','arm','aws-lambda'] + triggerFeatures, + [triggerFeatures], + [deployment], + [ctx.getBean(Arm), ctx.getBean(X86)], + [true, false], // cdk + CreateLambdaBuilderCommand.languagesForDeployment(deployment), + [CreateLambdaBuilderCommand.languagesForDeployment(deployment)], + [TestFramework.JUNIT, TestFramework.SPOCK, TestFramework.KOTEST], + [BuildTool.GRADLE, BuildTool.GRADLE_KOTLIN, BuildTool.MAVEN], + CreateLambdaBuilderCommand.jdkVersionsForDeployment(deployment), + [CreateLambdaBuilderCommand.jdkVersionsForDeployment(deployment)] + ].combinations().collect { new LambdaCliOptions(codingStyle, *it) } + } + + private static class LambdaCliOptions { + + final CodingStyle codingStyle + final Feature apiFeature + final LambdaDeployment lambdaDeployment + final CpuArchitecture cpuArchitecture + final boolean cdk + final Language language + final TestFramework testFramework + final BuildTool buildTool + final JdkVersion javaVersion + + final List allApiFeatures + final List allLanguages + final List allJdkVersions + + LambdaCliOptions( + CodingStyle codingStyle, + Feature apiFeatures, + List allApiFeatures, + LambdaDeployment lambdaDeployment, + CpuArchitecture cpuArchitecture, + boolean cdk, + Language language, + Language[] allLanguages, + TestFramework testFramework, + BuildTool buildTool, + JdkVersion javaVersion, + JdkVersion[] allJdkVersions + ) { + this.codingStyle = codingStyle + this.apiFeature = apiFeatures + this.allApiFeatures = allApiFeatures + this.lambdaDeployment = lambdaDeployment + this.cpuArchitecture = cpuArchitecture + this.cdk = cdk + this.language = language + this.allLanguages = allLanguages.toList() + this.testFramework = testFramework + this.buildTool = buildTool + this.javaVersion = javaVersion + this.allJdkVersions = allJdkVersions.toList() + } + + List getFeatures() { + List features = [ + AwsLambda.FEATURE_NAME_AWS_LAMBDA, + apiFeature.getName(), + ] + if (lambdaDeployment == LambdaDeployment.NATIVE_EXECUTABLE) { + features += [GraalVM.FEATURE_NAME_GRAALVM] + } + features += [cpuArchitecture.getName()] + if (cdk) { + features += [Cdk.NAME] + } + return features + } + + ApplicationType getExpectedApplicationType() { + if (codingStyle != CodingStyle.HANDLER) { + return ApplicationType.DEFAULT + } + return ApplicationType.FUNCTION + } + + List getCliCommands() { + [ + "${codingStyle.ordinal() + 1}".toString(), + "${allApiFeatures.indexOf(apiFeature) + 1}".toString(), + "${lambdaDeployment.ordinal() + 1}".toString(), + cpuArchitecture instanceof Arm ? "1" : "2", + cdk ? "1" : "2", + "${allLanguages.indexOf(language) + 1}".toString(), + "${testFramework.ordinal() + 1}".toString(), + "${buildTool.ordinal() + 1}".toString(), + "${allJdkVersions.indexOf(javaVersion) + 1}".toString() + ] + } + + @Override + String toString() { + "${codingStyle} ${apiFeature?.name} ${lambdaDeployment} ${cpuArchitecture.name} cdk:${cdk} $language $testFramework $buildTool $javaVersion" + } } } diff --git a/test-suite-graal/src/test/java/io/micronaut/starter/cli/command/CreateLambdaCommandCliOptions.java b/test-suite-graal/src/test/java/io/micronaut/starter/cli/command/CreateLambdaCommandCliOptions.java new file mode 100644 index 00000000000..bafeb493503 --- /dev/null +++ b/test-suite-graal/src/test/java/io/micronaut/starter/cli/command/CreateLambdaCommandCliOptions.java @@ -0,0 +1,122 @@ +package io.micronaut.starter.cli.command; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.starter.application.ApplicationType; +import io.micronaut.starter.feature.Feature; +import io.micronaut.starter.feature.architecture.Arm; +import io.micronaut.starter.feature.architecture.CpuArchitecture; +import io.micronaut.starter.feature.aws.Cdk; +import io.micronaut.starter.feature.function.awslambda.AwsLambda; +import io.micronaut.starter.feature.graalvm.GraalVM; +import io.micronaut.starter.options.BuildTool; +import io.micronaut.starter.options.JdkVersion; +import io.micronaut.starter.options.Language; +import io.micronaut.starter.options.TestFramework; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class CreateLambdaCommandCliOptions implements CommandSupplier { + + final CodingStyle codingStyle; + final Feature apiFeature; + final LambdaDeployment lambdaDeployment; + final CpuArchitecture cpuArchitecture; + final boolean cdk; + final Language language; + final TestFramework testFramework; + final BuildTool buildTool; + final JdkVersion javaVersion; + + final List allApiFeatures; + final List allLanguages; + final List allJdkVersions; + @Nullable final String expectedExceptionMessage; + + private int index = 0; + + CreateLambdaCommandCliOptions( + CodingStyle codingStyle, + Feature apiFeatures, + List allApiFeatures, + LambdaDeployment lambdaDeployment, + CpuArchitecture cpuArchitecture, + boolean cdk, + Language language, + Language[] allLanguages, + TestFramework testFramework, + BuildTool buildTool, + JdkVersion javaVersion, + JdkVersion[] allJdkVersions, + @Nullable String expectedExceptionMessage + ) { + this.codingStyle = codingStyle; + this.apiFeature = apiFeatures; + this.allApiFeatures = allApiFeatures; + this.lambdaDeployment = lambdaDeployment; + this.cpuArchitecture = cpuArchitecture; + this.cdk = cdk; + this.language = language; + this.allLanguages = Arrays.asList(allLanguages); + this.testFramework = testFramework; + this.buildTool = buildTool; + this.javaVersion = javaVersion; + this.allJdkVersions = Arrays.asList(allJdkVersions); + this.expectedExceptionMessage = expectedExceptionMessage; + } + + List getFeatures() { + List features = new ArrayList<>(); + features.add(AwsLambda.FEATURE_NAME_AWS_LAMBDA); + features.add(apiFeature.getName()); + if (lambdaDeployment == LambdaDeployment.NATIVE_EXECUTABLE) { + features.add(GraalVM.FEATURE_NAME_GRAALVM); + } + features.add(cpuArchitecture.getName()); + if (cdk) { + features.add(Cdk.NAME); + } + return features; + } + + ApplicationType getExpectedApplicationType() { + if (codingStyle != CodingStyle.HANDLER) { + return ApplicationType.DEFAULT; + } + return ApplicationType.FUNCTION; + } + + List getCliCommands() { + List commands = new ArrayList<>(); + commands.add("" + (codingStyle.ordinal() + 1)); + commands.add("" + (allApiFeatures.indexOf(apiFeature) + 1)); + commands.add("" + (lambdaDeployment.ordinal() + 1)); + commands.add(cpuArchitecture instanceof Arm ? "1" : "2"); + commands.add(cdk ? "1" : "2"); + commands.add("" + (allLanguages.indexOf(language) + 1)); + commands.add("" + (testFramework.ordinal() + 1)); + commands.add("" + (buildTool.ordinal() + 1)); + commands.add("" + (allJdkVersions.indexOf(javaVersion) + 1)); + return commands; + } + + @Override + public String toString() { + return "%s %s %s %s cdk:%s %s %s %s %s".formatted( + codingStyle, + apiFeature.getName(), + lambdaDeployment, + cpuArchitecture.getName(), + cdk, + language, + testFramework, + buildTool, + javaVersion + ); + } + + public String nextCommand() { + return getCliCommands().get(index++); + } +} diff --git a/test-suite-graal/src/test/java/io/micronaut/starter/cli/command/CreateLambdaCommandTest.java b/test-suite-graal/src/test/java/io/micronaut/starter/cli/command/CreateLambdaCommandTest.java new file mode 100644 index 00000000000..791c71761ac --- /dev/null +++ b/test-suite-graal/src/test/java/io/micronaut/starter/cli/command/CreateLambdaCommandTest.java @@ -0,0 +1,130 @@ +package io.micronaut.starter.cli.command; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.starter.application.ApplicationType; +import io.micronaut.starter.application.OperatingSystem; +import io.micronaut.starter.application.generator.ProjectGenerator; +import io.micronaut.starter.feature.Feature; +import io.micronaut.starter.feature.architecture.Arm; +import io.micronaut.starter.feature.architecture.X86; +import io.micronaut.starter.feature.aws.LambdaFunctionUrl; +import io.micronaut.starter.options.BuildTool; +import io.micronaut.starter.options.TestFramework; +import io.micronaut.starter.util.NameUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.HashSet; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CreateLambdaCommandTest { + + static ApplicationContext applicationContext; + + @BeforeAll + static void setUp() { + applicationContext = ApplicationContext.run(); + } + + @AfterAll + static void tearDown() { + if (applicationContext != null) { + applicationContext.close(); + } + } + + static Stream provideCliOptions() { + return Stream.of(CodingStyle.values()) + .flatMap(codingStyle -> Stream.of(LambdaDeployment.values()) + .flatMap(lambdaDeployment -> Stream.of(applicationContext.getBean(Arm.class), applicationContext.getBean(X86.class)) + .flatMap(cpuArchitecture -> Stream.of(false, true) + .flatMap(cdk -> Stream.of(CreateLambdaBuilderCommand.languagesForDeployment(lambdaDeployment)) + .flatMap(language -> Stream.of(TestFramework.JUNIT, TestFramework.SPOCK, TestFramework.KOTEST) + .flatMap(testFramework -> Stream.of(BuildTool.GRADLE, BuildTool.GRADLE_KOTLIN, BuildTool.MAVEN) + .flatMap(buildTool -> getAllApiFeatures(codingStyle).stream() + .flatMap(feature -> Stream.of(CreateLambdaBuilderCommand.jdkVersionsForDeployment(lambdaDeployment)) + .map(jdkVersion -> new CreateLambdaCommandCliOptions( + codingStyle, + feature, + getAllApiFeatures(codingStyle), + lambdaDeployment, + cpuArchitecture, + cdk, + language, + CreateLambdaBuilderCommand.languagesForDeployment(lambdaDeployment), + testFramework, + buildTool, + jdkVersion, + CreateLambdaBuilderCommand.jdkVersionsForDeployment(lambdaDeployment), + invalid(feature, cdk, buildTool, lambdaDeployment) + )) + ) + ) + ) + ) + ) + ) + ) + ); + } + + /** + * We don't support GraalVM with Maven and CDK, though the tool does allow this combination. + */ + private static String invalid(Feature feature, boolean cdk, BuildTool buildTool, LambdaDeployment lambdaDeployment) { + boolean hasCdk = feature instanceof LambdaFunctionUrl || cdk; + if (hasCdk && buildTool == BuildTool.MAVEN && lambdaDeployment == LambdaDeployment.NATIVE_EXECUTABLE) { + return "Maven, CDK and GraalVM are not yet supported"; + } + return null; + } + + private static List getAllApiFeatures(CodingStyle codingStyle) { + return codingStyle == CodingStyle.CONTROLLERS ? + CreateLambdaBuilderCommand.apiTriggerFeatures(ApplicationType.DEFAULT, applicationContext.getBeansOfType(Feature.class)) : + CreateLambdaBuilderCommand.triggerFeatures(applicationContext.getBeansOfType(Feature.class)); + } + + @ParameterizedTest + @MethodSource("provideCliOptions") + void testCliOptions(CreateLambdaCommandCliOptions cliOptions) { + CreateLambdaBuilderCommand command = applicationContext.getBean(CreateLambdaBuilderCommand.class); + ProjectGenerator projectGenerator = applicationContext.getBean(ProjectGenerator.class); + + GenerateOptions options = command.createGenerateOptions(new StubbedLineReader(cliOptions)); + + assertEquals(cliOptions.getExpectedApplicationType(), options.getApplicationType()); + assertEquals(new HashSet<>(cliOptions.getFeatures()), options.getFeatures()); + assertEquals(cliOptions.language, options.getOptions().getLanguage()); + assertEquals(cliOptions.buildTool, options.getOptions().getBuildTool()); + assertEquals(cliOptions.testFramework, options.getOptions().getTestFramework()); + assertEquals(cliOptions.javaVersion, options.getOptions().getJavaVersion()); + + if (cliOptions.expectedExceptionMessage != null) { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> generate(projectGenerator, options)); + assertEquals(cliOptions.expectedExceptionMessage, ex.getMessage()); + } else { + assertDoesNotThrow(() -> generate(projectGenerator, options) + ); + } + } + + private static void generate(ProjectGenerator projectGenerator, GenerateOptions options) throws Exception { + projectGenerator.generate( + options.getApplicationType(), + NameUtils.parse("foo"), + options.getOptions(), + OperatingSystem.LINUX, + options.getFeatures().stream().toList(), + new TemplateResolvingOutputHandler(), + new LoggingConsoleOutput() + ); + } +}