Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ data class KotlinDependency(
val IDENTITY_API = KotlinDependency(GradleConfiguration.Implementation, "$RUNTIME_ROOT_NS", RUNTIME_GROUP, "identity-api", RUNTIME_VERSION)
val SMITHY_RPCV2_PROTOCOLS = KotlinDependency(GradleConfiguration.Implementation, "$RUNTIME_ROOT_NS.awsprotocol.rpcv2", RUNTIME_GROUP, "smithy-rpcv2-protocols", RUNTIME_VERSION)
val SMITHY_RPCV2_PROTOCOLS_CBOR = KotlinDependency(GradleConfiguration.Implementation, "$RUNTIME_ROOT_NS.awsprotocol.rpcv2.cbor", RUNTIME_GROUP, "smithy-rpcv2-protocols", RUNTIME_VERSION)
val AWS_SIGNING_CRT = KotlinDependency(GradleConfiguration.Implementation, "$RUNTIME_ROOT_NS.auth.awssigning.crt", RUNTIME_GROUP, "aws-signing-crt", RUNTIME_VERSION)

// External third-party dependencies
val KOTLIN_STDLIB = KotlinDependency(GradleConfiguration.Implementation, "kotlin", "org.jetbrains.kotlin", "kotlin-stdlib", KOTLIN_COMPILER_VERSION)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ object RuntimeTypes {

object SmokeTests : RuntimeTypePackage(KotlinDependency.CORE, "smoketests") {
val exitProcess = symbol("exitProcess")
val printExceptionStackTrace = symbol("printExceptionStackTrace")
}

object Collections : RuntimeTypePackage(KotlinDependency.CORE, "collections") {
Expand Down Expand Up @@ -378,6 +379,10 @@ object RuntimeTypes {
val sigV4 = symbol("sigV4")
val sigV4A = symbol("sigV4A")
}

object AwsSigningCrt : RuntimeTypePackage(KotlinDependency.AWS_SIGNING_CRT) {
val CrtAwsSigner = symbol("CrtAwsSigner")
}
}

object Observability {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,53 @@ package software.amazon.smithy.kotlin.codegen.rendering.smoketests
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.integration.SectionId
import software.amazon.smithy.kotlin.codegen.integration.SectionKey
import software.amazon.smithy.kotlin.codegen.model.getTrait
import software.amazon.smithy.kotlin.codegen.model.hasTrait
import software.amazon.smithy.kotlin.codegen.model.isStringEnumShape
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointParametersGenerator
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointProviderGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.stringToNumber
import software.amazon.smithy.kotlin.codegen.rendering.smoketests.ClientConfig.EndpointParams
import software.amazon.smithy.kotlin.codegen.rendering.smoketests.ClientConfig.EndpointProvider
import software.amazon.smithy.kotlin.codegen.rendering.smoketests.ClientConfig.Name
import software.amazon.smithy.kotlin.codegen.rendering.smoketests.ClientConfig.Value
import software.amazon.smithy.kotlin.codegen.rendering.util.format
import software.amazon.smithy.kotlin.codegen.utils.dq
import software.amazon.smithy.kotlin.codegen.utils.toCamelCase
import software.amazon.smithy.kotlin.codegen.utils.toPascalCase
import software.amazon.smithy.kotlin.codegen.utils.topDownOperations
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.node.*
import software.amazon.smithy.model.shapes.*
import software.amazon.smithy.smoketests.traits.SmokeTestCase
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait
import kotlin.jvm.optionals.getOrNull

object SmokeTestsRunner : SectionId
object SmokeTestAdditionalEnvVars : SectionId
object SmokeTestDefaultConfig : SectionId
object SmokeTestRegionDefault : SectionId
object SmokeTestHttpEngineOverride : SectionId
// Section IDs
object AdditionalEnvironmentVariables : SectionId
object DefaultClientConfig : SectionId
object HttpEngineOverride : SectionId
object ServiceFilter : SectionId
object SkipTags : SectionId
object ClientConfig : SectionId {
val Name: SectionKey<String> = SectionKey("aws.smithy.kotlin#SmokeTestClientConfigName")
val Value: SectionKey<String> = SectionKey("aws.smithy.kotlin#SmokeTestClientConfigValue")
val EndpointProvider: SectionKey<Symbol> = SectionKey("aws.smithy.kotlin#SmokeTestEndpointProvider")
val EndpointParams: SectionKey<Symbol> = SectionKey("aws.smithy.kotlin#SmokeTestClientEndpointParams")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

organization/style: Namespace these sections under another object SmokeTestSectionId

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: My comment here is still valid

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't part of the diff anymore. But do you mean the SectionKeys?


/**
* Env var for 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 SMOKE_TEST_SKIP_TAGS, it MUST be skipped by the smoke test runner.
*/
const val SKIP_TAGS = "SMOKE_TEST_SKIP_TAGS"

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

/**
* Renders smoke tests runner for a service
Expand All @@ -30,36 +58,45 @@ class SmokeTestsRunnerGenerator(
private val writer: KotlinWriter,
ctx: CodegenContext,
) {
private val model = ctx.model
private val sdkId = ctx.settings.sdkId
private val symbolProvider = ctx.symbolProvider
private val service = symbolProvider.toSymbol(model.expectShape(ctx.settings.service))
private val operations = ctx.model.topDownOperations(ctx.settings.service).filter { it.hasTrait<SmokeTestsTrait>() }

internal fun render() {
writer.declareSection(SmokeTestsRunner) {
write("private var exitCode = 0")
write(
"private val skipTags = #T.System.getenv(#S)?.let { it.split(#S).map { it.trim() }.toSet() } ?: emptySet()",
RuntimeTypes.Core.Utils.PlatformProvider,
SKIP_TAGS,
",",
)
write(
"private val serviceFilter = #T.System.getenv(#S)?.let { it.split(#S).map { it.trim() }.toSet() } ?: emptySet()",
RuntimeTypes.Core.Utils.PlatformProvider,
SERVICE_FILTER,
",",
)
declareSection(SmokeTestAdditionalEnvVars)
write("")
withBlock("public suspend fun main() {", "}") {
renderFunctionCalls()
write("#T(exitCode)", RuntimeTypes.Core.SmokeTests.exitProcess)
}
write("")
renderFunctions()
writer.write("private var exitCode = 0")
renderEnvironmentVariables()
writer.declareSection(AdditionalEnvironmentVariables)
writer.write("")
writer.withBlock("public suspend fun main() {", "}") {
renderFunctionCalls()
write("#T(exitCode)", RuntimeTypes.Core.SmokeTests.exitProcess)
}
writer.write("")
renderFunctions()
}

private fun renderEnvironmentVariables() {
// Skip tags
writer.writeInline(
"private val skipTags = #T.System.getenv(",
RuntimeTypes.Core.Utils.PlatformProvider,
)
writer.declareSection(SkipTags) {
writer.writeInline("#S", SKIP_TAGS)
}
writer.write(
")?.let { it.split(#S).map { it.trim() }.toSet() } ?: emptySet()",
",",
)

// Service filter
writer.writeInline(
"private val serviceFilter = #T.System.getenv(",
RuntimeTypes.Core.Utils.PlatformProvider,
)
writer.declareSection(ServiceFilter) {
writer.writeInline("#S", SERVICE_FILTER)
}
writer.write(
")?.let { it.split(#S).map { it.trim() }.toSet() } ?: emptySet()",
",",
)
}

private fun renderFunctionCalls() {
Expand Down Expand Up @@ -98,32 +135,45 @@ class SmokeTestsRunnerGenerator(
renderClient(testCase)
renderOperation(operation, testCase)
}
withBlock("catch (e: Exception) {", "}") {
withBlock("catch (exception: Exception) {", "}") {
renderCatchBlock(testCase)
}
}
}

private fun renderClient(testCase: SmokeTestCase) {
writer.withInlineBlock("#L {", "}", service) {
if (testCase.vendorParams.isPresent) {
testCase.vendorParams.get().members.forEach { vendorParam ->
if (vendorParam.key.value == "region") {
writeInline("#L = ", vendorParam.key.value.toCamelCase())
declareSection(SmokeTestRegionDefault)
write("#L", vendorParam.value.format())
} else {
write("#L = #L", vendorParam.key.value.toCamelCase(), vendorParam.value.format())
}
}
} else {
declareSection(SmokeTestDefaultConfig)
}
val expectingSpecificError = testCase.expectation.failure.getOrNull()?.errorId?.getOrNull() != null
if (!expectingSpecificError) {
write("interceptors.add(#T())", RuntimeTypes.HttpClient.Interceptors.SmokeTestsInterceptor)
renderClientConfig(testCase)
}
}

private fun renderClientConfig(testCase: SmokeTestCase) {
if (!testCase.expectingSpecificError) {
writer.write("interceptors.add(#T())", RuntimeTypes.HttpClient.Interceptors.SmokeTestsInterceptor)
}

writer.declareSection(HttpEngineOverride)

if (!testCase.hasClientConfig) {
writer.declareSection(DefaultClientConfig)
return
}

testCase.clientConfig!!.forEach { config ->
val name = config.key.value.toCamelCase()
val value = config.value.format()

writer.declareSection(
ClientConfig,
mapOf(
Name to name,
Value to value,
EndpointProvider to EndpointProviderGenerator.getSymbol(settings),
EndpointParams to EndpointParametersGenerator.getSymbol(settings),
),
) {
writer.writeInline("#L = #L", name, value)
}
declareSection(SmokeTestHttpEngineOverride)
}
}

Expand All @@ -133,30 +183,97 @@ class SmokeTestsRunnerGenerator(
writer.withBlock(".#T { client ->", "}", RuntimeTypes.Core.IO.use) {
withBlock("client.#L(", ")", operation.defaultName()) {
withBlock("#L {", "}", operationSymbol) {
testCase.params.get().members.forEach { member ->
write("#L = #L", member.key.value.toCamelCase(), member.value.format())
}
renderOperationParameters(operation, testCase)
}
}
}
}

private fun renderOperationParameters(operation: OperationShape, testCase: SmokeTestCase) {
if (!testCase.hasOperationParameters) return

val paramsToShapes = mapOperationParametersToModeledShapes(operation)

testCase.operationParameters.forEach { param ->
val paramName = param.key.value.toCamelCase()
writer.writeInline("#L = ", paramName)
val paramShape = paramsToShapes[paramName] ?: throw IllegalArgumentException("Unable to find shape for operation parameter '$paramName' in smoke test '${testCase.functionName}'.")
renderOperationParameter(paramName, param.value, paramShape, testCase)
}
}

private fun renderCatchBlock(testCase: SmokeTestCase) {
val expected = if (testCase.expectation.isFailure) {
val expectedException = if (testCase.expectation.isFailure) {
getFailureCriterion(testCase)
} else {
RuntimeTypes.HttpClient.Interceptors.SmokeTestsSuccessException
}

writer.write("val success = e is #T", expected)
writer.write("val status = if (success) #S else #S", "ok", "not ok")
writer.write("val success: Boolean = exception is #T", expectedException)
writer.write("val status: String = if (success) #S else #S", "ok", "not ok")

printTestResult(
sdkId.filter { !it.isWhitespace() },
testCase.id,
testCase.expectation.isFailure,
writer,
)
writer.write("if (!success) exitCode = 1")

writer.withBlock("if (!success) {", "}") {
write("#T(exception)", RuntimeTypes.Core.SmokeTests.printExceptionStackTrace)
write("exitCode = 1")
}
}

// Helpers
/**
* Renders a [SmokeTestCase] operation parameter
*/
private fun renderOperationParameter(
paramName: String,
node: Node,
shape: Shape,
testCase: SmokeTestCase,
) {
when {
// String enum
node is StringNode && shape.isStringEnumShape -> {
val enumSymbol = symbolProvider.toSymbol(shape)
val enumValue = node.value.toPascalCase()
writer.write("#T.#L", enumSymbol, enumValue)
}
// Int enum
node is NumberNode && shape is IntEnumShape -> {
val enumSymbol = symbolProvider.toSymbol(shape)
val enumValue = node.format()
writer.write("#T.fromValue(#L.toInt())", enumSymbol, enumValue)
}
// Number
node is NumberNode && shape is NumberShape -> writer.write("#L.#L", node.format(), stringToNumber(shape))
// Object
node is ObjectNode -> {
val shapeSymbol = symbolProvider.toSymbol(shape)
writer.withBlock("#T {", "}", shapeSymbol) {
node.members.forEach { member ->
val memberName = member.key.value.toCamelCase()
val memberShape = shape.allMembers[member.key.value] ?: throw IllegalArgumentException("Unable to find shape for operation parameter '$paramName' in smoke test '${testCase.functionName}'.")
writer.writeInline("#L = ", memberName)
renderOperationParameter(memberName, member.value, memberShape, testCase)
}
}
}
// List
node is ArrayNode && shape is CollectionShape -> {
writer.withBlock("listOf(", ")") {
node.elements.forEach { element ->
renderOperationParameter(paramName, element, model.expectShape(shape.member.target), testCase)
writer.write(",")
}
}
}
// Everything else
else -> writer.write("#L", node.format())
Comment on lines +276 to +277
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we support "anything else" or should we be throwing an exception if we reach this else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we support "simple" types i.e. Node. If we encounter an unknown Node type we throw an exception.

}
}

/**
Expand Down Expand Up @@ -184,10 +301,56 @@ class SmokeTestsRunnerGenerator(
val testResult = "$status $service $testCase - $expectation $directive"
writer.write("println(#S)", testResult)
}
}

/**
* Derives a function name for a [SmokeTestCase]
*/
private val SmokeTestCase.functionName: String
get() = this.id.toCamelCase()
/**
* Maps an operations parameters to their shapes
*/
private fun mapOperationParametersToModeledShapes(operation: OperationShape): Map<String, Shape> =
model.getShape(operation.inputShape).get().allMembers.map { (key, value) ->
key.toCamelCase() to model.getShape(value.target).get()
}.toMap()

/**
* Derives a function name for a [SmokeTestCase]
*/
private val SmokeTestCase.functionName: String
get() = this.id.toCamelCase()

/**
* Get the operation parameters for a [SmokeTestCase]
*/
private val SmokeTestCase.operationParameters: Map<StringNode, Node>
get() = this.params.get().members

/**
* Checks if there are operation parameters for a [SmokeTestCase]
*/
private val SmokeTestCase.hasOperationParameters: Boolean
get() = this.params.isPresent

/**
* Check if a [SmokeTestCase] is expecting a specific error
*/
private val SmokeTestCase.expectingSpecificError: Boolean
get() = this.expectation.failure.getOrNull()?.errorId?.getOrNull() != null

/**
* Checks if a [SmokeTestCase] requires client configuration
*/
private val SmokeTestCase.hasClientConfig: Boolean
get() = this.vendorParams.isPresent

/**
* Get the client configuration required for a [SmokeTestCase]
*/
private val SmokeTestCase.clientConfig: MutableMap<StringNode, Node>?
get() = this.vendorParams.get().members

// Constants
private val model = ctx.model
private val settings = ctx.settings
private val sdkId = settings.sdkId
private val symbolProvider = ctx.symbolProvider
private val service = symbolProvider.toSymbol(model.expectShape(settings.service))
private val operations = model.topDownOperations(settings.service).filter { it.hasTrait<SmokeTestsTrait>() }
}
Loading
Loading