Skip to content

Commit 6933790

Browse files
authored
feat: aws smoke tests support (#1437)
1 parent 5793661 commit 6933790

File tree

17 files changed

+805
-41
lines changed

17 files changed

+805
-41
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ build/
1212
.idea/
1313
__pycache__/
1414
local.properties
15+
1516
# ignore generated files
1617
services/*/generated-src
1718
services/*/build.gradle.kts
1819
.kotest/
19-
*.klib
20+
*.klib
21+
tests/codegen/smoke-tests/services/*/generated-src
22+
tests/codegen/smoke-tests/services/*/build.gradle.kts

codegen/aws-sdk-codegen/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929
api(libs.smithy.aws.cloudformation.traits)
3030
api(libs.smithy.protocol.test.traits)
3131
implementation(libs.smithy.aws.endpoints)
32+
implementation(libs.smithy.smoke.test.traits)
3233

3334
testImplementation(libs.junit.jupiter)
3435
testImplementation(libs.junit.jupiter.params)

codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/GradleGenerator.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@
44
*/
55
package aws.sdk.kotlin.codegen
66

7+
import aws.sdk.kotlin.codegen.model.traits.testing.TestFailedResponseTrait
8+
import aws.sdk.kotlin.codegen.model.traits.testing.TestSuccessResponseTrait
79
import software.amazon.smithy.kotlin.codegen.core.*
810
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
11+
import software.amazon.smithy.kotlin.codegen.model.expectShape
12+
import software.amazon.smithy.kotlin.codegen.model.hasTrait
913
import software.amazon.smithy.kotlin.codegen.rendering.GradleWriter
14+
import software.amazon.smithy.kotlin.codegen.utils.topDownOperations
15+
import software.amazon.smithy.model.shapes.ServiceShape
16+
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait
1017

1118
// TODO - would be nice to allow integrations to define custom settings in the plugin
1219
// e.g. we could then more consistently apply this integration if we could define a property like: `build.isAwsSdk: true`
@@ -64,9 +71,72 @@ class GradleGenerator : KotlinIntegration {
6471
}
6572
}
6673
}
74+
if (ctx.model.topDownOperations(ctx.settings.service).any { it.hasTrait<SmokeTestsTrait>() }) {
75+
write("")
76+
generateSmokeTestConfig(writer, ctx)
77+
}
6778
}
6879

6980
val contents = writer.toString()
7081
delegator.fileManifest.writeFile("build.gradle.kts", contents)
7182
}
83+
84+
private fun generateSmokeTestConfig(writer: GradleWriter, ctx: CodegenContext) {
85+
generateSmokeTestJarTask(writer, ctx)
86+
writer.write("")
87+
generateSmokeTestTask(writer, ctx)
88+
}
89+
90+
/**
91+
* Generates a gradle task to create smoke test runner JARs
92+
*/
93+
private fun generateSmokeTestJarTask(writer: GradleWriter, ctx: CodegenContext) {
94+
writer.withBlock("jvm {", "}") {
95+
withBlock("compilations {", "}") {
96+
write("val mainPath = getByName(#S).output.classesDirs", "main")
97+
write("val testPath = getByName(#S).output.classesDirs", "test")
98+
withBlock("tasks {", "}") {
99+
withBlock("register<Jar>(#S) {", "}", "smokeTestJar") {
100+
write("description = #S", "Creates smoke tests jar")
101+
write("group = #S", "application")
102+
write("dependsOn(build)")
103+
write("mustRunAfter(build)")
104+
withBlock("manifest {", "}") {
105+
write("attributes[#S] = #S", "Main-Class", "${ctx.settings.pkg.name}.smoketests.SmokeTestsKt")
106+
}
107+
write("val runtimePath = configurations.getByName(#S).map { if (it.isDirectory) it else zipTree(it) }", "jvmRuntimeClasspath")
108+
write("duplicatesStrategy = DuplicatesStrategy.EXCLUDE")
109+
write("from(runtimePath, mainPath, testPath)")
110+
write("archiveBaseName.set(#S)", "\${project.name}-smoketests")
111+
}
112+
}
113+
}
114+
}
115+
}
116+
117+
/**
118+
* Generates a gradle task to run smoke tests
119+
*/
120+
private fun generateSmokeTestTask(writer: GradleWriter, ctx: CodegenContext) {
121+
val hasSuccessResponseTrait = ctx.model.expectShape<ServiceShape>(ctx.settings.service).hasTrait(TestSuccessResponseTrait.ID)
122+
val hasFailedResponseTrait = ctx.model.expectShape<ServiceShape>(ctx.settings.service).hasTrait(TestFailedResponseTrait.ID)
123+
val inTestingEnvironment = hasFailedResponseTrait || hasSuccessResponseTrait
124+
125+
/**
126+
* E2E tests don't have sdkVersion in jar names. They're added later for publishing.
127+
* @see SmokeTestE2ETest
128+
*/
129+
val jarName = if (inTestingEnvironment) "\${project.name}-smoketests.jar" else "\${project.name}-smoketests-\$sdkVersion.jar"
130+
131+
writer.withBlock("tasks.register<JavaExec>(#S) {", "}", "smokeTest") {
132+
write("description = #S", "Runs smoke tests jar")
133+
write("group = #S", "verification")
134+
write("dependsOn(tasks.getByName(#S))", "smokeTestJar")
135+
write("mustRunAfter(tasks.getByName(#S))", "smokeTestJar")
136+
write("")
137+
write("val sdkVersion: String by project")
138+
write("val jarFile = file(#S)", "build/libs/$jarName")
139+
write("classpath = files(jarFile)")
140+
}
141+
}
72142
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package aws.sdk.kotlin.codegen.model.traits.testing
2+
3+
import software.amazon.smithy.model.node.ObjectNode
4+
import software.amazon.smithy.model.shapes.ShapeId
5+
import software.amazon.smithy.model.traits.AnnotationTrait
6+
7+
/**
8+
* Indicates the annotated service should always return a failed response.
9+
* IMPORTANT: This trait is intended for use in integration or E2E tests only, not in real-life smoke tests that run
10+
* against a service endpoint.
11+
*/
12+
class TestFailedResponseTrait(node: ObjectNode) : AnnotationTrait(ID, node) {
13+
companion object {
14+
val ID: ShapeId = ShapeId.from("smithy.kotlin.traits#failedResponseTrait")
15+
}
16+
}
17+
18+
/**
19+
* Indicates the annotated service should always return a success response.
20+
* IMPORTANT: This trait is intended for use in integration or E2E tests only, not in real-life smoke tests that run
21+
* against a service endpoint.
22+
*/
23+
class TestSuccessResponseTrait(node: ObjectNode) : AnnotationTrait(ID, node) {
24+
companion object {
25+
val ID: ShapeId = ShapeId.from("smithy.kotlin.traits#successResponseTrait")
26+
}
27+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package aws.sdk.kotlin.codegen.smoketests
2+
3+
import aws.sdk.kotlin.codegen.AwsRuntimeTypes
4+
import software.amazon.smithy.kotlin.codegen.KotlinSettings
5+
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
6+
import software.amazon.smithy.kotlin.codegen.core.getContextValue
7+
import software.amazon.smithy.kotlin.codegen.core.withBlock
8+
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
9+
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
10+
import software.amazon.smithy.kotlin.codegen.model.hasTrait
11+
import software.amazon.smithy.kotlin.codegen.rendering.smoketests.*
12+
import software.amazon.smithy.kotlin.codegen.utils.topDownOperations
13+
import software.amazon.smithy.model.Model
14+
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait
15+
16+
/**
17+
* Generates AWS specific code for smoke test runners
18+
*/
19+
class AwsSmokeTestsRunnerGeneratorIntegration : KotlinIntegration {
20+
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
21+
model.topDownOperations(settings.service).any { it.hasTrait<SmokeTestsTrait>() }
22+
23+
override val sectionWriters: List<SectionWriterBinding>
24+
get() = listOf(
25+
AwsSmokeTestsRunnerGenerator.regionEnvironmentVariable,
26+
AwsSmokeTestsRunnerGenerator.clientConfig,
27+
AwsSmokeTestsRunnerGenerator.defaultClientConfig,
28+
AwsSmokeTestsRunnerGenerator.skipTagsEnvironmentVariable,
29+
AwsSmokeTestsRunnerGenerator.serviceFilterEnvironmentVariable,
30+
)
31+
}
32+
33+
/**
34+
* The section writer bindings used by [AwsSmokeTestsRunnerGeneratorIntegration]
35+
*/
36+
private object AwsSmokeTestsRunnerGenerator {
37+
/**
38+
* Adds region environment variable support to AWS smoke test runners.
39+
* Preserves other environment variables added via section writer binding, if any.
40+
*/
41+
val regionEnvironmentVariable =
42+
SectionWriterBinding(SmokeTestSectionIds.AdditionalEnvironmentVariables) { writer, previous ->
43+
writer.write("#L", previous)
44+
writer.write(
45+
"private val regionOverride = #T.System.getenv(#S)",
46+
RuntimeTypes.Core.Utils.PlatformProvider,
47+
AWS_REGION,
48+
)
49+
}
50+
51+
/**
52+
* Add AWS specific client config support to AWS smoke test runners
53+
*/
54+
val clientConfig =
55+
SectionWriterBinding(SmokeTestSectionIds.ClientConfig) { writer, _ ->
56+
val name = writer.getContextValue(SmokeTestSectionIds.ClientConfig.Name)
57+
val value = writer.getContextValue(SmokeTestSectionIds.ClientConfig.Value)
58+
59+
// Normalize client config names
60+
val newName = when (name) {
61+
"uri" -> "endpointProvider"
62+
"useDualstack" -> "useDualStack"
63+
"sigv4aRegionSet" -> "sigV4aSigningRegionSet"
64+
"useAccountIdRouting" -> "accountIdEndpointMode"
65+
"useAccelerate" -> "enableAccelerate"
66+
"useMultiRegionAccessPoints" -> "disableMrap"
67+
"useGlobalEndpoint" -> {
68+
writer.write("throw #T(#S)", RuntimeTypes.Core.SmokeTests.SmokeTestsException, "'useGlobalEndpoint' is not supported by the SDK")
69+
return@SectionWriterBinding
70+
}
71+
else -> name
72+
}
73+
writer.writeInline("#L = ", newName)
74+
75+
// Normalize client values
76+
when (newName) {
77+
"endpointProvider" -> {
78+
val endpointProvider = writer.getContextValue(SmokeTestSectionIds.ClientConfig.EndpointProvider)
79+
val endpointParameters = writer.getContextValue(SmokeTestSectionIds.ClientConfig.EndpointParams)
80+
81+
writer.withBlock("object : #T {", "}", endpointProvider) {
82+
write(
83+
"override suspend fun resolveEndpoint(params: #1T): #2T = #2T(#3L)",
84+
endpointParameters,
85+
RuntimeTypes.SmithyClient.Endpoints.Endpoint,
86+
value,
87+
)
88+
}
89+
}
90+
"sigV4aSigningRegionSet" -> {
91+
// Render new value
92+
writer.write("#L.toSet()", value)
93+
// Also configure sigV4a - TODO: Remove once sigV4a is supported for default signer.
94+
writer.write(
95+
"authSchemes = listOf(#T(#T))",
96+
RuntimeTypes.Auth.HttpAuthAws.SigV4AsymmetricAuthScheme,
97+
RuntimeTypes.Auth.AwsSigningCrt.CrtAwsSigner,
98+
)
99+
}
100+
"accountIdEndpointMode" -> {
101+
when (value) {
102+
"true" -> writer.write("#T.REQUIRED", AwsRuntimeTypes.Config.Endpoints.AccountIdEndpointMode)
103+
"false" -> writer.write("#T.DISABLED", AwsRuntimeTypes.Config.Endpoints.AccountIdEndpointMode)
104+
}
105+
}
106+
"disableMrap" -> {
107+
when (value) {
108+
"true" -> writer.write("false")
109+
"false" -> writer.write("true")
110+
}
111+
}
112+
"region" -> {
113+
writer.write("regionOverride ?: #L", value)
114+
}
115+
else -> writer.write("#L", value)
116+
}
117+
}
118+
119+
/**
120+
* Add default client config to AWS smoke test runners.
121+
* Preserves previous default config if any.
122+
*/
123+
val defaultClientConfig =
124+
SectionWriterBinding(SmokeTestSectionIds.DefaultClientConfig) { writer, previous ->
125+
writer.write("#L", previous)
126+
writer.write("region = regionOverride")
127+
}
128+
129+
/**
130+
* Replaces environment variable with one specific to AWS smoke test runners
131+
*/
132+
val skipTagsEnvironmentVariable =
133+
SectionWriterBinding(SmokeTestSectionIds.SkipTags) { writer, _ -> writer.writeInline("#S", AWS_SKIP_TAGS) }
134+
135+
/**
136+
* Replaces environment variable with one specific to AWS smoke test runners
137+
*/
138+
val serviceFilterEnvironmentVariable =
139+
SectionWriterBinding(SmokeTestSectionIds.ServiceFilter) { writer, _ -> writer.writeInline("#S", AWS_SERVICE_FILTER) }
140+
}
141+
142+
/**
143+
* Env var for AWS smoke test runners.
144+
* Should be a string that corresponds to an AWS region.
145+
* The region to use when executing smoke tests. This value MUST override any value supplied in the smoke tests themselves.
146+
*/
147+
private const val AWS_REGION = "AWS_SMOKE_TEST_REGION"
148+
149+
/**
150+
* Env var for AWS smoke test runners.
151+
* Should be a comma-delimited list of strings that correspond to tags on the test cases.
152+
* If a test case is tagged with one of the tags indicated by AWS_SMOKE_TEST_SKIP_TAGS, it MUST be skipped by the smoke test runner.
153+
*/
154+
const val AWS_SKIP_TAGS = "AWS_SMOKE_TEST_SKIP_TAGS"
155+
156+
/**
157+
* Env var for AWS smoke test runners.
158+
* Should be a comma-separated list of service identifiers to test.
159+
*/
160+
const val AWS_SERVICE_FILTER = "AWS_SMOKE_TEST_SERVICE_IDS"

codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/smoketests/SmokeTestsDenyListIntegration.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package aws.sdk.kotlin.codegen.smoketests.testing
2+
3+
import aws.sdk.kotlin.codegen.model.traits.testing.TestFailedResponseTrait
4+
import aws.sdk.kotlin.codegen.model.traits.testing.TestSuccessResponseTrait
5+
import software.amazon.smithy.kotlin.codegen.KotlinSettings
6+
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
7+
import software.amazon.smithy.kotlin.codegen.core.withBlock
8+
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
9+
import software.amazon.smithy.kotlin.codegen.integration.SectionWriter
10+
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
11+
import software.amazon.smithy.kotlin.codegen.model.expectShape
12+
import software.amazon.smithy.kotlin.codegen.model.hasTrait
13+
import software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestSectionIds
14+
import software.amazon.smithy.kotlin.codegen.utils.topDownOperations
15+
import software.amazon.smithy.model.Model
16+
import software.amazon.smithy.model.shapes.ServiceShape
17+
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait
18+
19+
/**
20+
* Adds [TestFailedResponseTrait] support to smoke tests
21+
* IMPORTANT: This integration is intended for use in integration or E2E tests only, not in real-life smoke tests that run
22+
* against a service endpoint.
23+
*/
24+
class SmokeTestFailHttpEngineIntegration : KotlinIntegration {
25+
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
26+
model.topDownOperations(settings.service).any { it.hasTrait<SmokeTestsTrait>() } &&
27+
!model.expectShape<ServiceShape>(settings.service).hasTrait(TestSuccessResponseTrait.ID) &&
28+
model.expectShape<ServiceShape>(settings.service).hasTrait(TestFailedResponseTrait.ID)
29+
30+
override val sectionWriters: List<SectionWriterBinding>
31+
get() = listOf(
32+
SectionWriterBinding(SmokeTestSectionIds.HttpEngineOverride, httpClientOverride),
33+
)
34+
35+
private val httpClientOverride = SectionWriter { writer, _ ->
36+
writer.withBlock("httpClient = #T(", ")", RuntimeTypes.HttpTest.TestEngine) {
37+
withBlock("roundTripImpl = { _, request ->", "}") {
38+
write(
39+
"val resp = #T(#T.BadRequest, #T.Empty, #T.Empty)",
40+
RuntimeTypes.Http.Response.HttpResponse,
41+
RuntimeTypes.Http.StatusCode,
42+
RuntimeTypes.Http.Headers,
43+
RuntimeTypes.Http.HttpBody,
44+
)
45+
write("val now = #T.now()", RuntimeTypes.Core.Instant)
46+
write("#T(request, resp, now, now)", RuntimeTypes.Http.HttpCall)
47+
}
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)