diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt index 524e1924a..17faf0ad4 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt @@ -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) 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 d37e47737..e42612b0b 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 @@ -116,6 +116,8 @@ object RuntimeTypes { object SmokeTests : RuntimeTypePackage(KotlinDependency.CORE, "smoketests") { val exitProcess = symbol("exitProcess") + val printExceptionStackTrace = symbol("printExceptionStackTrace") + val SmokeTestsException = symbol("SmokeTestsException") } object Collections : RuntimeTypePackage(KotlinDependency.CORE, "collections") { @@ -378,6 +380,10 @@ object RuntimeTypes { val sigV4 = symbol("sigV4") val sigV4A = symbol("sigV4A") } + + object AwsSigningCrt : RuntimeTypePackage(KotlinDependency.AWS_SIGNING_CRT) { + val CrtAwsSigner = symbol("CrtAwsSigner") + } } object Observability { 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 index edcf8b7b2..1b9aa8032 100644 --- 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 @@ -3,25 +3,55 @@ 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.SmokeTestSectionIds.ClientConfig.EndpointParams +import software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestSectionIds.ClientConfig.EndpointProvider +import software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestSectionIds.ClientConfig.Name +import software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestSectionIds.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 SmokeTestSectionIds { + object AdditionalEnvironmentVariables : SectionId + object DefaultClientConfig : SectionId + object HttpEngineOverride : SectionId + object ServiceFilter : SectionId + object SkipTags : SectionId + object ClientConfig : SectionId { + val Name: SectionKey = SectionKey("aws.smithy.kotlin#SmokeTestClientConfigName") + val Value: SectionKey = SectionKey("aws.smithy.kotlin#SmokeTestClientConfigValue") + val EndpointProvider: SectionKey = SectionKey("aws.smithy.kotlin#SmokeTestEndpointProvider") + val EndpointParams: SectionKey = SectionKey("aws.smithy.kotlin#SmokeTestClientEndpointParams") + } +} -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-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" + +/** + * 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 +60,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() } - 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(SmokeTestSectionIds.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(SmokeTestSectionIds.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(SmokeTestSectionIds.ServiceFilter) { + writer.writeInline("#S", SERVICE_FILTER) } + writer.write( + ")?.let { it.split(#S).map { it.trim() }.toSet() } ?: emptySet()", + ",", + ) } private fun renderFunctionCalls() { @@ -98,7 +137,7 @@ class SmokeTestsRunnerGenerator( renderClient(testCase) renderOperation(operation, testCase) } - withBlock("catch (e: Exception) {", "}") { + withBlock("catch (exception: Exception) {", "}") { renderCatchBlock(testCase) } } @@ -106,24 +145,37 @@ class SmokeTestsRunnerGenerator( 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(SmokeTestSectionIds.HttpEngineOverride) + + if (!testCase.hasClientConfig) { + writer.declareSection(SmokeTestSectionIds.DefaultClientConfig) + return + } + + testCase.clientConfig!!.forEach { config -> + val name = config.key.value.toCamelCase() + val value = config.value.format() + + writer.declareSection( + SmokeTestSectionIds.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 +185,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()) + } } /** @@ -184,10 +303,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 = + 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 + 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? + 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() } +} 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 index 1e7d3103c..73b9a041f 100644 --- 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 @@ -23,5 +23,5 @@ fun Node.format(): String = when (this) { is ObjectNode -> stringMap.entries.joinToString(", ", "mapOf(", ")") { (key, value) -> "${key.dq()} to ${value.format()}" } - else -> throw Exception("Unexpected node type: $this") + else -> throw IllegalStateException("Unexpected node type: $this") } 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 b4aac4e4f..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,4 +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 +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 index ddb5b3c1d..0f1d469eb 100644 --- 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 @@ -74,8 +74,8 @@ class SmokeTestsRunnerGeneratorTest { generatedCode.shouldContainOnlyOnceWithDiff( """ private var exitCode = 0 - private val skipTags = PlatformProvider.System.getenv("AWS_SMOKE_TEST_SKIP_TAGS")?.let { it.split(",").map { it.trim() }.toSet() } ?: emptySet() - private val serviceFilter = PlatformProvider.System.getenv("AWS_SMOKE_TEST_SERVICE_IDS")?.let { it.split(",").map { it.trim() }.toSet() } + private val skipTags = PlatformProvider.System.getenv("SMOKE_TEST_SKIP_TAGS")?.let { it.split(",").map { it.trim() }.toSet() } ?: emptySet() + private val serviceFilter = PlatformProvider.System.getenv("SMOKE_TEST_SERVICE_IDS")?.let { it.split(",").map { it.trim() }.toSet() } """.trimIndent(), ) } @@ -107,9 +107,8 @@ class SmokeTestsRunnerGeneratorTest { try { com.test.TestClient { - region = "eu-central-1" interceptors.add(SmokeTestsInterceptor()) - + region = "eu-central-1" }.use { client -> client.testOperation( com.test.model.TestOperationRequest { @@ -118,11 +117,14 @@ class SmokeTestsRunnerGeneratorTest { ) } - } catch (e: Exception) { - val success = e is SmokeTestsSuccessException - val status = if (success) "ok" else "not ok" + } catch (exception: Exception) { + val success: Boolean = exception is SmokeTestsSuccessException + val status: String = if (success) "ok" else "not ok" println("${'$'}status Test SuccessTest - no error expected from service ") - if (!success) exitCode = 1 + if (!success) { + printExceptionStackTrace(exception) + exitCode = 1 + } } } """.trimIndent(), @@ -151,11 +153,14 @@ class SmokeTestsRunnerGeneratorTest { ) } - } catch (e: Exception) { - val success = e is InvalidMessageError - val status = if (success) "ok" else "not ok" + } catch (exception: Exception) { + val success: Boolean = exception is InvalidMessageError + val status: String = if (success) "ok" else "not ok" println("${'$'}status Test InvalidMessageErrorTest - error expected from service ") - if (!success) exitCode = 1 + if (!success) { + printExceptionStackTrace(exception) + exitCode = 1 + } } } """.trimIndent(), @@ -185,11 +190,14 @@ class SmokeTestsRunnerGeneratorTest { ) } - } catch (e: Exception) { - val success = e is SmokeTestsFailureException - val status = if (success) "ok" else "not ok" + } catch (exception: Exception) { + val success: Boolean = exception is SmokeTestsFailureException + val status: String = if (success) "ok" else "not ok" println("${'$'}status Test FailureTest - error expected from service ") - if (!success) exitCode = 1 + if (!success) { + printExceptionStackTrace(exception) + exitCode = 1 + } } } """.trimIndent(), diff --git a/runtime/protocol/http-client/api/http-client.api b/runtime/protocol/http-client/api/http-client.api index c15a33b0b..6e91f528f 100644 --- a/runtime/protocol/http-client/api/http-client.api +++ b/runtime/protocol/http-client/api/http-client.api @@ -426,6 +426,7 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/ResponseLengthVal public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsFailureException : java/lang/Exception { public fun ()V + public fun (Ljava/lang/String;)V } public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { @@ -453,6 +454,7 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsInterce public final class aws/smithy/kotlin/runtime/http/interceptors/SmokeTestsSuccessException : java/lang/Exception { public fun ()V + public fun (Ljava/lang/String;)V } public final class aws/smithy/kotlin/runtime/http/middleware/DefaultValidateResponse : aws/smithy/kotlin/runtime/http/operation/ReceiveMiddleware { 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 index 63848ffe2..c8d6012d6 100644 --- 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 @@ -16,14 +16,19 @@ public class SmokeTestsInterceptor : HttpInterceptor { override fun readBeforeDeserialization(context: ProtocolResponseInterceptorContext) { val status = context.protocolResponse.status.value when (status) { - in 400..599 -> throw SmokeTestsFailureException() - in 200..299 -> throw SmokeTestsSuccessException() - else -> throw SmokeTestsUnexpectedException() + in 400..599 -> throw SmokeTestsFailureException("Smoke test failed with HTTP status code: $status") + in 200..299 -> throw SmokeTestsSuccessException("Smoke test succeeded with HTTP status code: $status") + else -> throw SmokeTestsUnexpectedException("Smoke test returned HTTP status code: $status") } } } -@InternalApi public class SmokeTestsFailureException : Exception() +@InternalApi public class SmokeTestsFailureException(message: String) : Exception(message) { + public constructor() : this("Smoke test failed with HTTP status code in the inclusive range: 400-599") +} + +@InternalApi public class SmokeTestsSuccessException(message: String) : Exception(message) { + public constructor() : this("Smoke test succeeded with HTTP status code in the inclusive range: 200-599") +} -@InternalApi public class SmokeTestsSuccessException : Exception() -private class SmokeTestsUnexpectedException : Exception() +private class SmokeTestsUnexpectedException(message: String) : Exception(message) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 5d90ebd3c..237aac6f7 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -2046,10 +2046,18 @@ public final class aws/smithy/kotlin/runtime/retries/policy/SuccessAcceptor : aw public final fun getSuccess ()Z } +public final class aws/smithy/kotlin/runtime/smoketests/SmokeTestsException : java/lang/Exception { + public fun (Ljava/lang/String;)V +} + public final class aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctionsJVMKt { public static final fun exitProcess (I)Ljava/lang/Void; } +public final class aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctionsKt { + public static final fun printExceptionStackTrace (Ljava/lang/Exception;)V +} + public final class aws/smithy/kotlin/runtime/text/Scanner { public fun (Ljava/lang/String;)V public final fun getText ()Ljava/lang/String; diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctions.kt index f996632e9..682817193 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smoketests/SmokeTestsFunctions.kt @@ -1,3 +1,21 @@ package aws.smithy.kotlin.runtime.smoketests public expect fun exitProcess(status: Int): Nothing + +/** + * Prints an exceptions stack trace using test anything protocol (TAP) format e.g. + * + * #java.lang.ArithmeticException: / by zero + * # at FileKt.main(File.kt:3) + * # at FileKt.main(File.kt) + * # at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + * # at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) + * # at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) + * # at java.base/java.lang.reflect.Method.invoke(Unknown Source) + * # at executors.JavaRunnerExecutor$Companion.main(JavaRunnerExecutor.kt:27) + * # at executors.JavaRunnerExecutor.main(JavaRunnerExecutor.kt) + */ +public fun printExceptionStackTrace(exception: Exception): Unit = + println(exception.stackTraceToString().split("\n").joinToString("\n") { "#$it" }) + +public class SmokeTestsException(message: String) : Exception(message)