-
Notifications
You must be signed in to change notification settings - Fork 31
misc: smoke tests fixes #1160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
misc: smoke tests fixes #1160
Changes from 16 commits
8cc53c3
d1fee9e
31c0a8f
2c6edb6
73d7bad
434eb22
2f47ed3
e7ac93f
b298270
5442f76
ffa8c50
1336afb
82c46e9
9c08472
d3abcca
44dfed5
04933e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -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") | ||||
| } | ||||
|
|
||||
| /** | ||||
| * 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 | ||||
|
|
@@ -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() { | ||||
|
|
@@ -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) | ||||
| } | ||||
| } | ||||
|
|
||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we support "simple" types i.e. Line 26 in f0df363
|
||||
| } | ||||
| } | ||||
|
|
||||
| /** | ||||
|
|
@@ -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>() } | ||||
| } | ||||
There was a problem hiding this comment.
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 SmokeTestSectionIdThere was a problem hiding this comment.
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
There was a problem hiding this comment.
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?