Skip to content

Commit

Permalink
feat: aws smoke tests support (#1437)
Browse files Browse the repository at this point in the history
  • Loading branch information
0marperez authored Nov 7, 2024
1 parent 5793661 commit 6933790
Show file tree
Hide file tree
Showing 17 changed files with 805 additions and 41 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ build/
.idea/
__pycache__/
local.properties

# ignore generated files
services/*/generated-src
services/*/build.gradle.kts
.kotest/
*.klib
*.klib
tests/codegen/smoke-tests/services/*/generated-src
tests/codegen/smoke-tests/services/*/build.gradle.kts
1 change: 1 addition & 0 deletions codegen/aws-sdk-codegen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
api(libs.smithy.aws.cloudformation.traits)
api(libs.smithy.protocol.test.traits)
implementation(libs.smithy.aws.endpoints)
implementation(libs.smithy.smoke.test.traits)

testImplementation(libs.junit.jupiter)
testImplementation(libs.junit.jupiter.params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@
*/
package aws.sdk.kotlin.codegen

import aws.sdk.kotlin.codegen.model.traits.testing.TestFailedResponseTrait
import aws.sdk.kotlin.codegen.model.traits.testing.TestSuccessResponseTrait
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.kotlin.codegen.model.hasTrait
import software.amazon.smithy.kotlin.codegen.rendering.GradleWriter
import software.amazon.smithy.kotlin.codegen.utils.topDownOperations
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait

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

val contents = writer.toString()
delegator.fileManifest.writeFile("build.gradle.kts", contents)
}

private fun generateSmokeTestConfig(writer: GradleWriter, ctx: CodegenContext) {
generateSmokeTestJarTask(writer, ctx)
writer.write("")
generateSmokeTestTask(writer, ctx)
}

/**
* Generates a gradle task to create smoke test runner JARs
*/
private fun generateSmokeTestJarTask(writer: GradleWriter, ctx: CodegenContext) {
writer.withBlock("jvm {", "}") {
withBlock("compilations {", "}") {
write("val mainPath = getByName(#S).output.classesDirs", "main")
write("val testPath = getByName(#S).output.classesDirs", "test")
withBlock("tasks {", "}") {
withBlock("register<Jar>(#S) {", "}", "smokeTestJar") {
write("description = #S", "Creates smoke tests jar")
write("group = #S", "application")
write("dependsOn(build)")
write("mustRunAfter(build)")
withBlock("manifest {", "}") {
write("attributes[#S] = #S", "Main-Class", "${ctx.settings.pkg.name}.smoketests.SmokeTestsKt")
}
write("val runtimePath = configurations.getByName(#S).map { if (it.isDirectory) it else zipTree(it) }", "jvmRuntimeClasspath")
write("duplicatesStrategy = DuplicatesStrategy.EXCLUDE")
write("from(runtimePath, mainPath, testPath)")
write("archiveBaseName.set(#S)", "\${project.name}-smoketests")
}
}
}
}
}

/**
* Generates a gradle task to run smoke tests
*/
private fun generateSmokeTestTask(writer: GradleWriter, ctx: CodegenContext) {
val hasSuccessResponseTrait = ctx.model.expectShape<ServiceShape>(ctx.settings.service).hasTrait(TestSuccessResponseTrait.ID)
val hasFailedResponseTrait = ctx.model.expectShape<ServiceShape>(ctx.settings.service).hasTrait(TestFailedResponseTrait.ID)
val inTestingEnvironment = hasFailedResponseTrait || hasSuccessResponseTrait

/**
* E2E tests don't have sdkVersion in jar names. They're added later for publishing.
* @see SmokeTestE2ETest
*/
val jarName = if (inTestingEnvironment) "\${project.name}-smoketests.jar" else "\${project.name}-smoketests-\$sdkVersion.jar"

writer.withBlock("tasks.register<JavaExec>(#S) {", "}", "smokeTest") {
write("description = #S", "Runs smoke tests jar")
write("group = #S", "verification")
write("dependsOn(tasks.getByName(#S))", "smokeTestJar")
write("mustRunAfter(tasks.getByName(#S))", "smokeTestJar")
write("")
write("val sdkVersion: String by project")
write("val jarFile = file(#S)", "build/libs/$jarName")
write("classpath = files(jarFile)")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package aws.sdk.kotlin.codegen.model.traits.testing

import software.amazon.smithy.model.node.ObjectNode
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.traits.AnnotationTrait

/**
* Indicates the annotated service should always return a failed response.
* IMPORTANT: This trait is intended for use in integration or E2E tests only, not in real-life smoke tests that run
* against a service endpoint.
*/
class TestFailedResponseTrait(node: ObjectNode) : AnnotationTrait(ID, node) {
companion object {
val ID: ShapeId = ShapeId.from("smithy.kotlin.traits#failedResponseTrait")
}
}

/**
* Indicates the annotated service should always return a success response.
* IMPORTANT: This trait is intended for use in integration or E2E tests only, not in real-life smoke tests that run
* against a service endpoint.
*/
class TestSuccessResponseTrait(node: ObjectNode) : AnnotationTrait(ID, node) {
companion object {
val ID: ShapeId = ShapeId.from("smithy.kotlin.traits#successResponseTrait")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package aws.sdk.kotlin.codegen.smoketests

import aws.sdk.kotlin.codegen.AwsRuntimeTypes
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
import software.amazon.smithy.kotlin.codegen.core.getContextValue
import software.amazon.smithy.kotlin.codegen.core.withBlock
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
import software.amazon.smithy.kotlin.codegen.model.hasTrait
import software.amazon.smithy.kotlin.codegen.rendering.smoketests.*
import software.amazon.smithy.kotlin.codegen.utils.topDownOperations
import software.amazon.smithy.model.Model
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait

/**
* Generates AWS specific code for smoke test runners
*/
class AwsSmokeTestsRunnerGeneratorIntegration : KotlinIntegration {
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
model.topDownOperations(settings.service).any { it.hasTrait<SmokeTestsTrait>() }

override val sectionWriters: List<SectionWriterBinding>
get() = listOf(
AwsSmokeTestsRunnerGenerator.regionEnvironmentVariable,
AwsSmokeTestsRunnerGenerator.clientConfig,
AwsSmokeTestsRunnerGenerator.defaultClientConfig,
AwsSmokeTestsRunnerGenerator.skipTagsEnvironmentVariable,
AwsSmokeTestsRunnerGenerator.serviceFilterEnvironmentVariable,
)
}

/**
* The section writer bindings used by [AwsSmokeTestsRunnerGeneratorIntegration]
*/
private object AwsSmokeTestsRunnerGenerator {
/**
* Adds region environment variable support to AWS smoke test runners.
* Preserves other environment variables added via section writer binding, if any.
*/
val regionEnvironmentVariable =
SectionWriterBinding(SmokeTestSectionIds.AdditionalEnvironmentVariables) { writer, previous ->
writer.write("#L", previous)
writer.write(
"private val regionOverride = #T.System.getenv(#S)",
RuntimeTypes.Core.Utils.PlatformProvider,
AWS_REGION,
)
}

/**
* Add AWS specific client config support to AWS smoke test runners
*/
val clientConfig =
SectionWriterBinding(SmokeTestSectionIds.ClientConfig) { writer, _ ->
val name = writer.getContextValue(SmokeTestSectionIds.ClientConfig.Name)
val value = writer.getContextValue(SmokeTestSectionIds.ClientConfig.Value)

// Normalize client config names
val newName = when (name) {
"uri" -> "endpointProvider"
"useDualstack" -> "useDualStack"
"sigv4aRegionSet" -> "sigV4aSigningRegionSet"
"useAccountIdRouting" -> "accountIdEndpointMode"
"useAccelerate" -> "enableAccelerate"
"useMultiRegionAccessPoints" -> "disableMrap"
"useGlobalEndpoint" -> {
writer.write("throw #T(#S)", RuntimeTypes.Core.SmokeTests.SmokeTestsException, "'useGlobalEndpoint' is not supported by the SDK")
return@SectionWriterBinding
}
else -> name
}
writer.writeInline("#L = ", newName)

// Normalize client values
when (newName) {
"endpointProvider" -> {
val endpointProvider = writer.getContextValue(SmokeTestSectionIds.ClientConfig.EndpointProvider)
val endpointParameters = writer.getContextValue(SmokeTestSectionIds.ClientConfig.EndpointParams)

writer.withBlock("object : #T {", "}", endpointProvider) {
write(
"override suspend fun resolveEndpoint(params: #1T): #2T = #2T(#3L)",
endpointParameters,
RuntimeTypes.SmithyClient.Endpoints.Endpoint,
value,
)
}
}
"sigV4aSigningRegionSet" -> {
// Render new value
writer.write("#L.toSet()", value)
// Also configure sigV4a - TODO: Remove once sigV4a is supported for default signer.
writer.write(
"authSchemes = listOf(#T(#T))",
RuntimeTypes.Auth.HttpAuthAws.SigV4AsymmetricAuthScheme,
RuntimeTypes.Auth.AwsSigningCrt.CrtAwsSigner,
)
}
"accountIdEndpointMode" -> {
when (value) {
"true" -> writer.write("#T.REQUIRED", AwsRuntimeTypes.Config.Endpoints.AccountIdEndpointMode)
"false" -> writer.write("#T.DISABLED", AwsRuntimeTypes.Config.Endpoints.AccountIdEndpointMode)
}
}
"disableMrap" -> {
when (value) {
"true" -> writer.write("false")
"false" -> writer.write("true")
}
}
"region" -> {
writer.write("regionOverride ?: #L", value)
}
else -> writer.write("#L", value)
}
}

/**
* Add default client config to AWS smoke test runners.
* Preserves previous default config if any.
*/
val defaultClientConfig =
SectionWriterBinding(SmokeTestSectionIds.DefaultClientConfig) { writer, previous ->
writer.write("#L", previous)
writer.write("region = regionOverride")
}

/**
* Replaces environment variable with one specific to AWS smoke test runners
*/
val skipTagsEnvironmentVariable =
SectionWriterBinding(SmokeTestSectionIds.SkipTags) { writer, _ -> writer.writeInline("#S", AWS_SKIP_TAGS) }

/**
* Replaces environment variable with one specific to AWS smoke test runners
*/
val serviceFilterEnvironmentVariable =
SectionWriterBinding(SmokeTestSectionIds.ServiceFilter) { writer, _ -> writer.writeInline("#S", AWS_SERVICE_FILTER) }
}

/**
* Env var for AWS smoke test runners.
* Should be a string that corresponds to an AWS region.
* The region to use when executing smoke tests. This value MUST override any value supplied in the smoke tests themselves.
*/
private const val AWS_REGION = "AWS_SMOKE_TEST_REGION"

/**
* Env var for AWS smoke test runners.
* Should be a comma-delimited list of strings that correspond to tags on the test cases.
* 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.
*/
const val AWS_SKIP_TAGS = "AWS_SMOKE_TEST_SKIP_TAGS"

/**
* Env var for AWS smoke test runners.
* Should be a comma-separated list of service identifiers to test.
*/
const val AWS_SERVICE_FILTER = "AWS_SMOKE_TEST_SERVICE_IDS"

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package aws.sdk.kotlin.codegen.smoketests.testing

import aws.sdk.kotlin.codegen.model.traits.testing.TestFailedResponseTrait
import aws.sdk.kotlin.codegen.model.traits.testing.TestSuccessResponseTrait
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
import software.amazon.smithy.kotlin.codegen.core.withBlock
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.integration.SectionWriter
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.kotlin.codegen.model.hasTrait
import software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestSectionIds
import software.amazon.smithy.kotlin.codegen.utils.topDownOperations
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait

/**
* Adds [TestFailedResponseTrait] support to smoke tests
* IMPORTANT: This integration is intended for use in integration or E2E tests only, not in real-life smoke tests that run
* against a service endpoint.
*/
class SmokeTestFailHttpEngineIntegration : KotlinIntegration {
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
model.topDownOperations(settings.service).any { it.hasTrait<SmokeTestsTrait>() } &&
!model.expectShape<ServiceShape>(settings.service).hasTrait(TestSuccessResponseTrait.ID) &&
model.expectShape<ServiceShape>(settings.service).hasTrait(TestFailedResponseTrait.ID)

override val sectionWriters: List<SectionWriterBinding>
get() = listOf(
SectionWriterBinding(SmokeTestSectionIds.HttpEngineOverride, httpClientOverride),
)

private val httpClientOverride = SectionWriter { writer, _ ->
writer.withBlock("httpClient = #T(", ")", RuntimeTypes.HttpTest.TestEngine) {
withBlock("roundTripImpl = { _, request ->", "}") {
write(
"val resp = #T(#T.BadRequest, #T.Empty, #T.Empty)",
RuntimeTypes.Http.Response.HttpResponse,
RuntimeTypes.Http.StatusCode,
RuntimeTypes.Http.Headers,
RuntimeTypes.Http.HttpBody,
)
write("val now = #T.now()", RuntimeTypes.Core.Instant)
write("#T(request, resp, now, now)", RuntimeTypes.Http.HttpCall)
}
}
}
}
Loading

0 comments on commit 6933790

Please sign in to comment.