From 0ffb5a530dcfefe38dd7b2d2716228c44f0680a3 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Fri, 23 Aug 2024 10:08:07 -0400 Subject: [PATCH] feat: smoke tests --- .../smithy-kotlin-codegen/build.gradle.kts | 1 + .../kotlin/codegen/core/KotlinDelegator.kt | 7 +- .../kotlin/codegen/core/KotlinWriter.kt | 2 + .../kotlin/codegen/core/RuntimeTypes.kt | 5 + .../codegen/rendering/GradleGenerator.kt | 2 + .../smoketests/SmokeTestsIntegration.kt | 39 ++++ .../smoketests/SmokeTestsRunnerGenerator.kt | 172 +++++++++++++++ .../kotlin/codegen/rendering/util/Node.kt | 31 +++ .../smithy/kotlin/codegen/utils/Model.kt | 16 ++ ...tlin.codegen.integration.KotlinIntegration | 1 + .../SmokeTestsRunnerGeneratorTest.kt | 200 ++++++++++++++++++ .../protocol/http-client/api/http-client.api | 39 ++++ .../interceptors/SmokeTestsInterceptor.kt | 39 ++++ 13 files changed, 551 insertions(+), 3 deletions(-) create mode 100644 codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsIntegration.kt create mode 100644 codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGenerator.kt create mode 100644 codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/Node.kt create mode 100644 codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/utils/Model.kt create mode 100644 codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGeneratorTest.kt create mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsInterceptor.kt diff --git a/codegen/smithy-kotlin-codegen/build.gradle.kts b/codegen/smithy-kotlin-codegen/build.gradle.kts index 8c8a662f7..956418641 100644 --- a/codegen/smithy-kotlin-codegen/build.gradle.kts +++ b/codegen/smithy-kotlin-codegen/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(libs.smithy.aws.traits) implementation(libs.smithy.protocol.traits) implementation(libs.smithy.protocol.test.traits) + implementation(libs.smithy.smoke.test.traits) implementation(libs.jsoup) // Test dependencies diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt index 7b5b871e6..ffe268c75 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt @@ -121,9 +121,10 @@ class KotlinDelegator( * * @param filename Name of the file to create. * @param block Lambda that accepts and works with the file. + * @param sourceSetRoot Root directory for source set */ - fun useFileWriter(filename: String, namespace: String, block: (KotlinWriter) -> Unit) { - val writer: KotlinWriter = checkoutWriter(filename, namespace) + fun useFileWriter(filename: String, namespace: String, sourceSetRoot: String = DEFAULT_SOURCE_SET_ROOT, block: (KotlinWriter) -> Unit) { + val writer: KotlinWriter = checkoutWriter(filename, namespace, sourceSetRoot) block(writer) } @@ -205,6 +206,6 @@ internal data class GeneratedDependency( } fun KotlinDelegator.useFileWriter(symbol: Symbol, block: (KotlinWriter) -> Unit) = - useFileWriter("${symbol.name}.kt", symbol.namespace, block) + useFileWriter("${symbol.name}.kt", symbol.namespace, DEFAULT_SOURCE_SET_ROOT, block) fun KotlinDelegator.applyFileWriter(symbol: Symbol, block: KotlinWriter.() -> Unit) = useFileWriter(symbol, block) diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt index 6fb4c81cf..b0a4e46bc 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt @@ -198,6 +198,8 @@ class KotlinWriter( ) } + fun emptyLine(): KotlinWriter = this.write("") + /** * Clean/escape any content from the doc that would invalidate the Kotlin output. */ diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt index d25e1c01c..4f1a3812f 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt @@ -86,6 +86,11 @@ object RuntimeTypes { val FlexibleChecksumsResponseInterceptor = symbol("FlexibleChecksumsResponseInterceptor") val ResponseLengthValidationInterceptor = symbol("ResponseLengthValidationInterceptor") val RequestCompressionInterceptor = symbol("RequestCompressionInterceptor") + val SmokeTestsInterceptor = symbol("SmokeTestsInterceptor") + val SmokeTestsFailureException = symbol("SmokeTestsFailureException") + val SmokeTestsSuccessException = symbol("SmokeTestsSuccessException") + val SmokeTestsUnexpectedException = symbol("SmokeTestsUnexpectedException") + val exitProcess = symbol("exitProcess") } } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/GradleGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/GradleGenerator.kt index dcaacf2f3..9be396903 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/GradleGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/GradleGenerator.kt @@ -250,4 +250,6 @@ class GradleWriter(parent: GradleWriter? = null) : AbstractCodeWriter() } && !smokeTestDenyList.contains(settings.sdkId) + + override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) = + delegator.useFileWriter("SmokeTests.kt", "smoketests", "./jvm-src/main/java/") { writer -> + SmokeTestsRunnerGenerator( + writer, + ctx.symbolProvider.toSymbol(ctx.model.expectShape(ctx.settings.service)), + ctx.model.operations(ctx.settings.service).filter { it.hasTrait() }, + ctx.model, + ctx.symbolProvider, + ctx.settings.sdkId, + ).render() + } +} + +/** + * SDK ID's of services that model smoke tests incorrectly + */ +val smokeTestDenyList = setOf( + "Application Auto Scaling", + "SWF", + "WAFV2", +) diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGenerator.kt new file mode 100644 index 000000000..62bdf0141 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGenerator.kt @@ -0,0 +1,172 @@ +package software.amazon.smithy.kotlin.codegen.rendering.smoketests + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.codegen.core.SymbolProvider +import software.amazon.smithy.kotlin.codegen.core.* +import software.amazon.smithy.kotlin.codegen.model.getTrait +import software.amazon.smithy.kotlin.codegen.rendering.util.render +import software.amazon.smithy.kotlin.codegen.utils.dq +import software.amazon.smithy.kotlin.codegen.utils.getOrNull +import software.amazon.smithy.kotlin.codegen.utils.toCamelCase +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.smoketests.traits.SmokeTestCase +import software.amazon.smithy.smoketests.traits.SmokeTestsTrait +import kotlin.jvm.optionals.getOrNull + +/** + * Renders smoke tests runner for a service + */ +class SmokeTestsRunnerGenerator( + private val writer: KotlinWriter, + private val service: Symbol, + private val operations: List, + private val model: Model, + private val symbolProvider: SymbolProvider, + private val sdkId: String, +) { + internal fun render() { + writer.write("private var exitCode = 0") + writer.write("private val regionOverride = System.getenv(\"AWS_SMOKE_TEST_REGION\")") + writer.write("private val skipTags = System.getenv(\"AWS_SMOKE_TEST_SKIP_TAGS\")?.let { it.split(\",\").map { it.trim() }.toSet() }") + writer.emptyLine() + writer.withBlock("public suspend fun main() {", "}") { + renderFunctionCalls() + write("#L(exitCode)", RuntimeTypes.HttpClient.Interceptors.exitProcess) + } + writer.emptyLine() + renderFunctions() + } + + private fun renderFunctionCalls() { + operations.forEach { operation -> + operation.getTrait()?.testCases?.forEach { testCase -> + writer.write("${testCase.id.toCamelCase()}()") + } + } + } + + private fun renderFunctions() { + operations.forEach { operation -> + operation.getTrait()?.testCases?.forEach { testCase -> + renderFunction(operation, testCase) + writer.emptyLine() + } + } + } + + private fun renderFunction(operation: OperationShape, testCase: SmokeTestCase) { + writer.withBlock("private suspend fun ${testCase.id.toCamelCase()}() {", "}") { + write("val tags = setOf(${testCase.tags.joinToString(",") { it.dq()} })") + write("if (skipTags != null && tags.any { it in skipTags }) return") + emptyLine() + withInlineBlock("try {", "} ") { + renderClient(testCase) + renderOperation(operation, testCase) + } + withBlock("catch (e: 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") { + write("region = regionOverride ?: #L", vendorParam.value.render()) + } else { + write("#L = #L", vendorParam.key.value.toCamelCase(), vendorParam.value.render()) + } + } + } else { + write("region = regionOverride") + } + val expectingSpecificError = testCase.expectation.failure.getOrNull()?.errorId?.getOrNull() != null + write("interceptors.add(#T($expectingSpecificError))", RuntimeTypes.HttpClient.Interceptors.SmokeTestsInterceptor) + } + checkVendorParamsAreCorrect(testCase) + } + + /** + * Smithy IDL doesn't check that vendor params are found in vendor params shape so we have to check here. + */ + private fun checkVendorParamsAreCorrect(testCase: SmokeTestCase) { + if (testCase.vendorParamsShape.isPresent && testCase.vendorParams.isPresent) { + val vendorParamsShape = model.getShape(testCase.vendorParamsShape.get()).get() + val validVendorParams = vendorParamsShape.members().map { it.memberName } + val vendorParams = testCase.vendorParams.get().members.map { it.key.value } + + vendorParams.forEach { vendorParam -> + check(validVendorParams.contains(vendorParam)) { + "Smithy smoke test \"${testCase.id}\" contains invalid vendor param \"$vendorParam\", it was not found in vendor params shape \"${testCase.vendorParamsShape}\"" + } + } + } + } + + private fun renderOperation(operation: OperationShape, testCase: SmokeTestCase) { + val operationSymbol = symbolProvider.toSymbol(model.getShape(operation.input.get()).get()) + + writer.addImport(operationSymbol) + writer.withBlock(".use { client ->", "}") { + withBlock("client.#L(", ")", operation.defaultName()) { + withBlock("#L {", "}", operationSymbol.name) { + testCase.params.get().members.forEach { member -> + write("#L = #L", member.key.value.toCamelCase(), member.value.render()) + } + } + } + } + } + + private fun renderCatchBlock(testCase: SmokeTestCase) { + val successCriterion = RuntimeTypes.HttpClient.Interceptors.SmokeTestsSuccessException + val failureCriterion = getFailureCriterion(testCase) + val expected = if (testCase.expectation.isFailure) { + failureCriterion + } else { + successCriterion + } + + writer.write("val success = e is #L", expected) + writer.write("val status = if (success) \"ok\" else \"not ok\"") + printTestResult( + sdkId.filter { !it.isWhitespace() }, + testCase.id, + testCase.expectation.isFailure, + writer, + ) + writer.write("if (!success) exitCode = 1") + } + + /** + * Tries to get the specific exception required in the failure criterion of a test. + * If no specific exception is required we default to the generic smoke tests failure exception. + * + * Some smoke tests model exceptions not found in the model, in that case we default to the generic smoke tests + * failure exception. + */ + private fun getFailureCriterion(testCase: SmokeTestCase): Symbol = testCase.expectation.failure.getOrNull()?.errorId?.let { + try { + symbolProvider.toSymbol(model.getShape(it.get()).get()) + } catch (e: Exception) { + RuntimeTypes.HttpClient.Interceptors.SmokeTestsFailureException + } + } ?: RuntimeTypes.HttpClient.Interceptors.SmokeTestsFailureException + + /** + * Renders print statement for smoke test result in accordance to design doc & test anything protocol (TAP) + */ + private fun printTestResult( + service: String, + testCase: String, + errorExpected: Boolean, + writer: KotlinWriter, + ) { + val expectation = if (errorExpected) "error expected from service" else "no error expected from service" + val testResult = "\$status $service $testCase - $expectation" + writer.write("println(#S)", testResult) + } +} diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/Node.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/Node.kt new file mode 100644 index 000000000..71a75af92 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/Node.kt @@ -0,0 +1,31 @@ +package software.amazon.smithy.kotlin.codegen.rendering.util + +import software.amazon.smithy.kotlin.codegen.utils.dq +import software.amazon.smithy.model.node.ArrayNode +import software.amazon.smithy.model.node.BooleanNode +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.NullNode +import software.amazon.smithy.model.node.NumberNode +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.model.node.StringNode + +/** + * Renders a [Node] into String format for codegen. + */ +fun Node.render(): String = when (this) { + is NullNode -> "null" + is StringNode -> value.dq() + is BooleanNode -> value.toString() + is NumberNode -> value.toString() + is ArrayNode -> elements.joinToString(",", "listOf(", ")") { element -> + element.render() + } + is ObjectNode -> buildString { + append("mapOf(") + stringMap.forEach { (key, value) -> + append("\t${key.dq()} to ${value.render()}") + } + append(")") + } + else -> throw Exception("Unexpected node type: $this") +} diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/utils/Model.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/utils/Model.kt new file mode 100644 index 000000000..613b9aebb --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/utils/Model.kt @@ -0,0 +1,16 @@ +package software.amazon.smithy.kotlin.codegen.utils + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.TopDownIndex +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.utils.SmithyInternalApi + +/** + * Syntactic sugar for getting a services operations + */ +@SmithyInternalApi +fun Model.operations(service: ShapeId): Set { + val topDownIndex = TopDownIndex.of(this) + return topDownIndex.getContainedOperations(service) +} diff --git a/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index a230428ee..0d02b5118 100644 --- a/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -12,3 +12,4 @@ software.amazon.smithy.kotlin.codegen.rendering.endpoints.discovery.EndpointDisc software.amazon.smithy.kotlin.codegen.rendering.endpoints.SdkEndpointBuiltinIntegration software.amazon.smithy.kotlin.codegen.rendering.compression.RequestCompressionIntegration software.amazon.smithy.kotlin.codegen.rendering.auth.SigV4AsymmetricAuthSchemeIntegration +software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestsIntegration diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGeneratorTest.kt new file mode 100644 index 000000000..eb6d6fc19 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGeneratorTest.kt @@ -0,0 +1,200 @@ +package software.amazon.smithy.kotlin.codegen.rendering.smoketests + +import software.amazon.smithy.kotlin.codegen.model.hasTrait +import software.amazon.smithy.kotlin.codegen.test.* +import software.amazon.smithy.kotlin.codegen.utils.operations +import software.amazon.smithy.model.Model +import software.amazon.smithy.smoketests.traits.SmokeTestsTrait +import kotlin.test.Test + +class SmokeTestsRunnerGeneratorTest { + private val moneySign = "$" + + private fun codegen(model: Model): String { + val testCtx = model.newTestContext() + val codegenCtx = testCtx.toCodegenContext() + val writer = testCtx.newWriter() + SmokeTestsRunnerGenerator( + writer, + codegenCtx.symbolProvider.toSymbol(codegenCtx.model.expectShape(codegenCtx.settings.service)), + codegenCtx.model.operations(codegenCtx.settings.service).filter { it.hasTrait() }, + codegenCtx.model, + codegenCtx.symbolProvider, + codegenCtx.settings.sdkId, + ).render() + return writer.toString() + } + + @Test + fun codegenTest() { + val model = + """ + ${moneySign}version: "2" + namespace com.test + + use smithy.test#smokeTests + + service Test { + version: "1.0.0", + operations: [ TestOperation ], + } + + @smokeTests( + [ + { + id: "SuccessTest" + params: {bar: "2"} + tags: [ + "success" + ] + expect: { + success: {} + } + vendorParamsShape: AwsVendorParams, + vendorParams: { + region: "eu-central-1" + } + } + { + id: "InvalidMessageErrorTest" + params: {bar: "föö"} + expect: { + failure: {errorId: InvalidMessageError} + } + } + { + id: "FailureTest" + params: {bar: "föö"} + expect: { + failure: {} + } + } + ] + ) + + operation TestOperation { + input := { + bar: String + } + errors: [ + InvalidMessageError + ] + } + + @error("client") + structure InvalidMessageError {} + + structure AwsVendorParams { + region: String + } + """.toSmithyModel() + + val generatedCode = codegen(model) + + generatedCode.shouldContainOnlyOnceWithDiff( + """ + private var exitCode = 0 + private val regionOverride = System.getenv("AWS_SMOKE_TEST_REGION") + private val skipTags = System.getenv("AWS_SMOKE_TEST_SKIP_TAGS")?.let { it.split(",").map { it.trim() }.toSet() } + """.trimIndent(), + ) + + generatedCode.shouldContainOnlyOnceWithDiff( + """ + public suspend fun main() { + successTest() + invalidMessageErrorTest() + failureTest() + aws.smithy.kotlin.runtime.http.interceptors.exitProcess(exitCode) + } + """.trimIndent(), + ) + + generatedCode.shouldContainOnlyOnceWithDiff( + """ + private suspend fun successTest() { + val tags = setOf("success") + if (skipTags != null && tags.any { it in skipTags }) return + + try { + com.test.TestClient { + region = regionOverride ?: "eu-central-1" + interceptors.add(SmokeTestsInterceptor(false)) + + }.use { client -> + client.testOperation( + TestOperationRequest { + bar = "2" + } + ) + } + + } catch (e: Exception) { + val success = e is aws.smithy.kotlin.runtime.http.interceptors.SmokeTestsSuccessException + val status = if (success) "ok" else "not ok" + println("${moneySign}status Test SuccessTest - no error expected from service") + if (!success) exitCode = 1 + } + } + """.trimIndent(), + ) + + generatedCode.shouldContainOnlyOnceWithDiff( + """ + private suspend fun invalidMessageErrorTest() { + val tags = setOf() + if (skipTags != null && tags.any { it in skipTags }) return + + try { + com.test.TestClient { + region = regionOverride + interceptors.add(SmokeTestsInterceptor(true)) + + }.use { client -> + client.testOperation( + TestOperationRequest { + bar = "föö" + } + ) + } + + } catch (e: Exception) { + val success = e is com.test.model.InvalidMessageError + val status = if (success) "ok" else "not ok" + println("${moneySign}status Test InvalidMessageErrorTest - error expected from service") + if (!success) exitCode = 1 + } + } + """.trimIndent(), + ) + + generatedCode.shouldContainOnlyOnceWithDiff( + """ + private suspend fun failureTest() { + val tags = setOf() + if (skipTags != null && tags.any { it in skipTags }) return + + try { + com.test.TestClient { + region = regionOverride + interceptors.add(SmokeTestsInterceptor(false)) + + }.use { client -> + client.testOperation( + TestOperationRequest { + bar = "föö" + } + ) + } + + } catch (e: Exception) { + val success = e is aws.smithy.kotlin.runtime.http.interceptors.SmokeTestsFailureException + val status = if (success) "ok" else "not ok" + println("${moneySign}status Test FailureTest - error expected from service") + if (!success) exitCode = 1 + } + } + """.trimIndent(), + ) + } +} diff --git a/runtime/protocol/http-client/api/http-client.api b/runtime/protocol/http-client/api/http-client.api index 9927ae030..288d1b13f 100644 --- a/runtime/protocol/http-client/api/http-client.api +++ b/runtime/protocol/http-client/api/http-client.api @@ -424,6 +424,45 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/ResponseLengthVal public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } +public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsFailureException : java/lang/Exception { + public fun ()V +} + +public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { + public fun (Z)V + public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeRetryLoop (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun readAfterAttempt (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterDeserialization (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterExecution (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readAfterSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readAfterTransmit (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V + public fun readBeforeAttempt (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V + public fun readBeforeExecution (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V + public fun readBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V + public fun readBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V +} + +public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsInterceptorKt { + public static final fun exitProcess (I)Ljava/lang/Void; +} + +public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsSuccessException : java/lang/Exception { + public fun ()V +} + +public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsUnexpectedException : java/lang/Exception { + public fun ()V +} + public final class aws/smithy/kotlin/runtime/http/middleware/DefaultValidateResponse : aws/smithy/kotlin/runtime/http/operation/ReceiveMiddleware { public fun ()V public fun handle (Laws/smithy/kotlin/runtime/http/operation/OperationRequest;Laws/smithy/kotlin/runtime/io/Handler;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsInterceptor.kt new file mode 100644 index 000000000..3828fc649 --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsInterceptor.kt @@ -0,0 +1,39 @@ +package aws.smithy.kotlin.runtime.http.interceptors + +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.client.ProtocolResponseInterceptorContext +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.response.HttpResponse +import kotlin.system.exitProcess + +/** + * Interceptor for smoke test runner clients. + * + * A passing test is not predicated on an SDK being able to parse the server response received, it’s based on the + * response’s HTTP status code UNLESS we're expecting a specific error. + */ +@InternalApi +public class SmokeTestsInterceptor( + private val expectingSpecificError: Boolean, +) : HttpInterceptor { + override fun readBeforeDeserialization(context: ProtocolResponseInterceptorContext) { + if (expectingSpecificError) return + val status = context.protocolResponse.status.value + when (status) { + in 400..599 -> throw SmokeTestsFailureException() + in 200..299 -> throw SmokeTestsSuccessException() + else -> throw SmokeTestsUnexpectedException() + } + } +} + +@InternalApi public class SmokeTestsFailureException : Exception() + +@InternalApi public class SmokeTestsSuccessException : Exception() + +@InternalApi public class SmokeTestsUnexpectedException : Exception() + +/** + * Runtime function to exit smoke test runners with a specific status code. + */ +@InternalApi public fun exitProcess(status: Int): Nothing = exitProcess(status)