From e56f0dd632ad815294727a9a1f6307d4eff5e0c8 Mon Sep 17 00:00:00 2001 From: david-perez Date: Thu, 27 Jun 2024 15:24:01 +0200 Subject: [PATCH] Refactor and DRY up protocol test generation (#3713) Protocol test generation code was one of the earlier parts of smithy-rs's codebase and it has accrued a fair amount of tech debt as we have evolved the code generation primitives. Its code was also forked when the server code generator was introduced, introducing a lot of duplicated code that has deviated over time. This commit refactors the code to modern standards and aims to reconcile commonalities in `ProtocolTestGenerator`, so that both client and server can reap centralized improvements over time. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._ --- .../rustsdk/AwsFluentClientDecorator.kt | 6 +- .../client/smithy/ClientCodegenVisitor.kt | 4 +- .../customize/ClientCodegenDecorator.kt | 2 +- .../smithy/customize/ConditionalDecorator.kt | 2 +- ...ator.kt => ClientProtocolTestGenerator.kt} | 290 +------- .../rust/codegen/core/rustlang/RustType.kt | 9 +- .../rust/codegen/core/rustlang/RustWriter.kt | 14 +- .../protocol/ProtocolTestGenerator.kt | 348 ++++++++++ .../smithy/PythonServerCodegenVisitor.kt | 6 +- .../server/smithy/ServerCodegenVisitor.kt | 19 +- .../protocol/ServerProtocolTestGenerator.kt | 647 +++++------------- 11 files changed, 608 insertions(+), 739 deletions(-) rename codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/{ProtocolTestGenerator.kt => ClientProtocolTestGenerator.kt} (61%) create mode 100644 codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/protocol/ProtocolTestGenerator.kt diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt index 95e754ff0c..7b14bf4414 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt @@ -13,8 +13,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.generators.client.Fluen import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientDocs import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientGenerator import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientSection -import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.DefaultProtocolTestGenerator -import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.ProtocolTestGenerator +import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.ClientProtocolTestGenerator import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.Feature import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter @@ -28,6 +27,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomization import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsSection +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolTestGenerator import software.amazon.smithy.rust.codegen.core.util.letIf import software.amazon.smithy.rust.codegen.core.util.serviceNameOrDefault import software.amazon.smithy.rustsdk.customize.s3.S3ExpressFluentClientCustomization @@ -91,7 +91,7 @@ class AwsFluentClientDecorator : ClientCodegenDecorator { codegenContext: ClientCodegenContext, baseGenerator: ProtocolTestGenerator, ): ProtocolTestGenerator = - DefaultProtocolTestGenerator( + ClientProtocolTestGenerator( codegenContext, baseGenerator.protocolSupport, baseGenerator.operationShape, diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientCodegenVisitor.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientCodegenVisitor.kt index d057b7e6f5..7e8737f50d 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientCodegenVisitor.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/ClientCodegenVisitor.kt @@ -23,7 +23,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationGen import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceGenerator import software.amazon.smithy.rust.codegen.client.smithy.generators.error.ErrorGenerator import software.amazon.smithy.rust.codegen.client.smithy.generators.error.OperationErrorGenerator -import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.DefaultProtocolTestGenerator +import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.ClientProtocolTestGenerator import software.amazon.smithy.rust.codegen.client.smithy.protocols.ClientProtocolLoader import software.amazon.smithy.rust.codegen.client.smithy.transformers.AddErrorMessage import software.amazon.smithy.rust.codegen.client.smithy.transformers.RemoveEventStreamOperations @@ -322,7 +322,7 @@ class ClientCodegenVisitor( // render protocol tests into `operation.rs` (note operationWriter vs. inputWriter) codegenDecorator.protocolTestGenerator( codegenContext, - DefaultProtocolTestGenerator( + ClientProtocolTestGenerator( codegenContext, protocolGeneratorFactory.support(), operationShape, diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt index c676e5259a..c4aec33b59 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt @@ -16,10 +16,10 @@ import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationGen import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginCustomization import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.generators.error.ErrorCustomization -import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.ProtocolTestGenerator import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.smithy.customize.CombinedCoreCodegenDecorator import software.amazon.smithy.rust.codegen.core.smithy.customize.CoreCodegenDecorator +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolTestGenerator import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolMap import java.util.ServiceLoader import java.util.logging.Logger diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ConditionalDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ConditionalDecorator.kt index 355d49b41f..d0a66c6b6a 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ConditionalDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ConditionalDecorator.kt @@ -17,7 +17,6 @@ import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationCus import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginCustomization import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.generators.error.ErrorCustomization -import software.amazon.smithy.rust.codegen.client.smithy.generators.protocol.ProtocolTestGenerator import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocCustomization import software.amazon.smithy.rust.codegen.core.smithy.generators.BuilderCustomization @@ -25,6 +24,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsCustomiza import software.amazon.smithy.rust.codegen.core.smithy.generators.ManifestCustomizations import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureCustomization import software.amazon.smithy.rust.codegen.core.smithy.generators.error.ErrorImplCustomization +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolTestGenerator /** * Delegating decorator that only applies when a condition is true diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ClientProtocolTestGenerator.kt similarity index 61% rename from codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt rename to codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ClientProtocolTestGenerator.kt index cb961cbd19..24c5fa5d67 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ProtocolTestGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/protocol/ClientProtocolTestGenerator.kt @@ -5,43 +5,37 @@ package software.amazon.smithy.rust.codegen.client.smithy.generators.protocol -import software.amazon.smithy.codegen.core.CodegenException -import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.shapes.DoubleShape import software.amazon.smithy.model.shapes.FloatShape import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.protocoltests.traits.AppliesTo -import software.amazon.smithy.protocoltests.traits.HttpMessageTestCase import software.amazon.smithy.protocoltests.traits.HttpRequestTestCase -import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait import software.amazon.smithy.protocoltests.traits.HttpResponseTestCase -import software.amazon.smithy.protocoltests.traits.HttpResponseTestsTrait import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule import software.amazon.smithy.rust.codegen.client.smithy.generators.ClientInstantiator -import software.amazon.smithy.rust.codegen.core.rustlang.Attribute -import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.allow import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.escape import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.rustlang.withBlock import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.FailingTest import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolSupport +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolTestGenerator +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId.AWS_JSON_10 +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.TestCase +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.TestCaseKind +import software.amazon.smithy.rust.codegen.core.util.PANIC import software.amazon.smithy.rust.codegen.core.util.dq -import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.inputShape import software.amazon.smithy.rust.codegen.core.util.isStreaming import software.amazon.smithy.rust.codegen.core.util.orNull import software.amazon.smithy.rust.codegen.core.util.outputShape -import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import java.util.logging.Logger import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType as RT @@ -52,18 +46,10 @@ data class ClientCreationParams( val clientName: String, ) -interface ProtocolTestGenerator { - val codegenContext: ClientCodegenContext - val protocolSupport: ProtocolSupport - val operationShape: OperationShape - - fun render(writer: RustWriter) -} - /** - * Generate protocol tests for an operation + * Generate client protocol tests for an [operationShape]. */ -class DefaultProtocolTestGenerator( +class ClientProtocolTestGenerator( override val codegenContext: ClientCodegenContext, override val protocolSupport: ProtocolSupport, override val operationShape: OperationShape, @@ -79,121 +65,53 @@ class DefaultProtocolTestGenerator( "Client" to ClientRustModule.root.toType().resolve("Client"), ) }, -) : ProtocolTestGenerator { +) : ProtocolTestGenerator() { + companion object { + private val ExpectFail = + setOf( + // Failing because we don't serialize default values if they match the default. + FailingTest(AWS_JSON_10, "AwsJson10ClientPopulatesDefaultsValuesWhenMissingInResponse", TestCaseKind.Request), + FailingTest(AWS_JSON_10, "AwsJson10ClientUsesExplicitlyProvidedMemberValuesOverDefaults", TestCaseKind.Request), + FailingTest(AWS_JSON_10, "AwsJson10ClientPopulatesDefaultValuesInInput", TestCaseKind.Request), + ) + } + + override val appliesTo: AppliesTo + get() = AppliesTo.CLIENT + override val expectFail: Set + get() = ExpectFail + override val runOnly: Set + get() = emptySet() + override val disabledTests: Set + get() = emptySet() + + override val logger: Logger = Logger.getLogger(javaClass.name) + private val rc = codegenContext.runtimeConfig - private val logger = Logger.getLogger(javaClass.name) private val inputShape = operationShape.inputShape(codegenContext.model) private val outputShape = operationShape.outputShape(codegenContext.model) - private val operationSymbol = codegenContext.symbolProvider.toSymbol(operationShape) - private val operationIndex = OperationIndex.of(codegenContext.model) private val instantiator = ClientInstantiator(codegenContext) private val codegenScope = arrayOf( - "SmithyHttp" to RT.smithyHttp(rc), "AssertEq" to RT.PrettyAssertions.resolve("assert_eq!"), "Uri" to RT.Http.resolve("Uri"), ) - sealed class TestCase { - abstract val testCase: HttpMessageTestCase - - data class RequestTest(override val testCase: HttpRequestTestCase) : TestCase() - - data class ResponseTest(override val testCase: HttpResponseTestCase, val targetShape: StructureShape) : - TestCase() - } - - override fun render(writer: RustWriter) { - val requestTests = - operationShape.getTrait() - ?.getTestCasesFor(AppliesTo.CLIENT).orEmpty().map { TestCase.RequestTest(it) } - val responseTests = - operationShape.getTrait() - ?.getTestCasesFor(AppliesTo.CLIENT).orEmpty().map { TestCase.ResponseTest(it, outputShape) } - val errorTests = - operationIndex.getErrors(operationShape).flatMap { error -> - val testCases = - error.getTrait() - ?.getTestCasesFor(AppliesTo.CLIENT).orEmpty() - testCases.map { TestCase.ResponseTest(it, error) } - } - val allTests: List = (requestTests + responseTests + errorTests).filterMatching() - if (allTests.isNotEmpty()) { - val operationName = operationSymbol.name - val testModuleName = "${operationName.toSnakeCase()}_request_test" - val additionalAttributes = - listOf( - Attribute(allow("unreachable_code", "unused_variables")), - ) - writer.withInlineModule( - RustModule.inlineTests(testModuleName, additionalAttributes = additionalAttributes), - null, - ) { - renderAllTestCases(allTests) - } - } - } - - private fun RustWriter.renderAllTestCases(allTests: List) { - allTests.forEach { - renderTestCaseBlock(it.testCase, this) { + override fun RustWriter.renderAllTestCases(allTests: List) { + for (it in allTests) { + renderTestCaseBlock(it, this) { when (it) { is TestCase.RequestTest -> this.renderHttpRequestTestCase(it.testCase) is TestCase.ResponseTest -> this.renderHttpResponseTestCase(it.testCase, it.targetShape) + is TestCase.MalformedRequestTest -> PANIC("Client protocol test generation does not support HTTP compliance test case type `$it`") } } } } - /** - * Filter out test cases that are disabled or don't match the service protocol - */ - private fun List.filterMatching(): List { - return if (RunOnly.isNullOrEmpty()) { - this.filter { testCase -> - testCase.testCase.protocol == codegenContext.protocol && - !DisableTests.contains(testCase.testCase.id) - } - } else { - this.filter { RunOnly.contains(it.testCase.id) } - } - } - - private fun renderTestCaseBlock( - testCase: HttpMessageTestCase, - testModuleWriter: RustWriter, - block: Writable, - ) { - testModuleWriter.newlinePrefix = "/// " - testCase.documentation.map { - testModuleWriter.writeWithNoFormatting(it) - } - testModuleWriter.write("Test ID: ${testCase.id}") - testModuleWriter.newlinePrefix = "" - Attribute.TokioTest.render(testModuleWriter) - val action = - when (testCase) { - is HttpResponseTestCase -> Action.Response - is HttpRequestTestCase -> Action.Request - else -> throw CodegenException("unknown test case type") - } - if (expectFail(testCase)) { - testModuleWriter.writeWithNoFormatting("#[should_panic]") - } - val fnName = - when (action) { - is Action.Response -> "_response" - is Action.Request -> "_request" - } - Attribute.AllowUnusedMut.render(testModuleWriter) - testModuleWriter.rustBlock("async fn ${testCase.id.toSnakeCase()}$fnName()") { - block(this) - } - } - private fun RustWriter.renderHttpRequestTestCase(httpRequestTestCase: HttpRequestTestCase) { if (!protocolSupport.requestSerialization) { rust("/* test case disabled for this protocol (not yet supported) */") @@ -276,18 +194,6 @@ class DefaultProtocolTestGenerator( } } - private fun HttpMessageTestCase.action(): Action = - when (this) { - is HttpRequestTestCase -> Action.Request - is HttpResponseTestCase -> Action.Response - else -> throw CodegenException("Unknown test case type") - } - - private fun expectFail(testCase: HttpMessageTestCase): Boolean = - ExpectFail.find { - it.id == testCase.id && it.action == testCase.action() && it.service == codegenContext.serviceShape.id.toString() - } != null - private fun RustWriter.renderHttpResponseTestCase( testCase: HttpResponseTestCase, expectedShape: StructureShape, @@ -434,58 +340,6 @@ class DefaultProtocolTestGenerator( } } - private fun checkRequiredHeaders( - rustWriter: RustWriter, - actualExpression: String, - requireHeaders: List, - ) { - basicCheck( - requireHeaders, - rustWriter, - "required_headers", - actualExpression, - "require_headers", - ) - } - - private fun checkForbidHeaders( - rustWriter: RustWriter, - actualExpression: String, - forbidHeaders: List, - ) { - basicCheck( - forbidHeaders, - rustWriter, - "forbidden_headers", - actualExpression, - "forbid_headers", - ) - } - - private fun checkHeaders( - rustWriter: RustWriter, - actualExpression: String, - headers: Map, - ) { - if (headers.isEmpty()) { - return - } - val variableName = "expected_headers" - rustWriter.withBlock("let $variableName = [", "];") { - writeWithNoFormatting( - headers.entries.joinToString(",") { - "(${it.key.dq()}, ${it.value.dq()})" - }, - ) - } - assertOk(rustWriter) { - write( - "#T($actualExpression, $variableName)", - RT.protocolTest(rc, "validate_headers"), - ) - } - } - private fun checkRequiredQueryParams( rustWriter: RustWriter, requiredParams: List, @@ -518,80 +372,4 @@ class DefaultProtocolTestGenerator( "&http_request", "validate_query_string", ) - - private fun basicCheck( - params: List, - rustWriter: RustWriter, - expectedVariableName: String, - actualExpression: String, - checkFunction: String, - ) { - if (params.isEmpty()) { - return - } - rustWriter.withBlock("let $expectedVariableName = ", ";") { - strSlice(this, params) - } - assertOk(rustWriter) { - write( - "#T($actualExpression, $expectedVariableName)", - RT.protocolTest(rc, checkFunction), - ) - } - } - - /** - * wraps `inner` in a call to `aws_smithy_protocol_test::assert_ok`, a convenience wrapper - * for pretty printing protocol test helper results - */ - private fun assertOk( - rustWriter: RustWriter, - inner: Writable, - ) { - rustWriter.write("#T(", RT.protocolTest(rc, "assert_ok")) - inner(rustWriter) - rustWriter.write(");") - } - - private fun strSlice( - writer: RustWriter, - args: List, - ) { - writer.withBlock("&[", "]") { - write(args.joinToString(",") { it.dq() }) - } - } - - companion object { - sealed class Action { - object Request : Action() - - object Response : Action() - } - - data class FailingTest(val service: String, val id: String, val action: Action) - - // These tests fail due to shortcomings in our implementation. - // These could be configured via runtime configuration, but since this won't be long-lasting, - // it makes sense to do the simplest thing for now. - // The test will _fail_ if these pass, so we will discover & remove if we fix them by accident - private val JsonRpc10 = "aws.protocoltests.json10#JsonRpc10" - private val AwsJson11 = "aws.protocoltests.json#JsonProtocol" - private val RestJson = "aws.protocoltests.restjson#RestJson" - private val RestXml = "aws.protocoltests.restxml#RestXml" - private val AwsQuery = "aws.protocoltests.query#AwsQuery" - private val Ec2Query = "aws.protocoltests.ec2#AwsEc2" - private val ExpectFail = - setOf( - // Failing because we don't serialize default values if they match the default - FailingTest(JsonRpc10, "AwsJson10ClientPopulatesDefaultsValuesWhenMissingInResponse", Action.Request), - FailingTest(JsonRpc10, "AwsJson10ClientUsesExplicitlyProvidedMemberValuesOverDefaults", Action.Request), - FailingTest(JsonRpc10, "AwsJson10ClientPopulatesDefaultValuesInInput", Action.Request), - ) - private val RunOnly: Set? = null - - // These tests are not even attempted to be generated, either because they will not compile - // or because they are flaky - private val DisableTests: Set = setOf() - } } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt index 10c8def399..6c57f5fd65 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustType.kt @@ -565,11 +565,16 @@ class Attribute(val inner: Writable, val isDeriveHelper: Boolean = false) { val DocInline = Attribute(doc("inline")) val NoImplicitPrelude = Attribute("no_implicit_prelude") - fun shouldPanic(expectedMessage: String) = - Attribute(macroWithArgs("should_panic", "expected = ${expectedMessage.dq()}")) + fun shouldPanic(expectedMessage: String? = null): Attribute = + if (expectedMessage != null) { + Attribute(macroWithArgs("should_panic", "expected = ${expectedMessage.dq()}")) + } else { + Attribute("should_panic") + } val Test = Attribute("test") val TokioTest = Attribute(RuntimeType.Tokio.resolve("test").writable) + val TracedTest = Attribute(RuntimeType.TracingTest.resolve("traced_test").writable) val AwsSdkUnstableAttribute = Attribute(cfg("aws_sdk_unstable")) /** diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt index 94b3dc67c6..80ae291537 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt @@ -378,7 +378,13 @@ fun > T.docs( vararg args: Any, newlinePrefix: String = "/// ", trimStart: Boolean = true, + /** If `false`, will disable templating in `args` into `#{T}` spans */ + templating: Boolean = true, ): T { + if (!templating && args.isNotEmpty()) { + PANIC("Templating was disabled yet the following arguments were passed in: $args") + } + // Because writing docs relies on the newline prefix, ensure that there was a new line written // before we write the docs this.ensureNewline() @@ -392,7 +398,13 @@ fun > T.docs( else -> it }.replace("\t", " ") // Rustdoc warns on tabs in documentation } - write(cleaned, *args) + + if (templating) { + write(cleaned, *args) + } else { + writeWithNoFormatting(cleaned) + } + popState() return this } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/protocol/ProtocolTestGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/protocol/ProtocolTestGenerator.kt new file mode 100644 index 0000000000..2202063a56 --- /dev/null +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/protocol/ProtocolTestGenerator.kt @@ -0,0 +1,348 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.core.smithy.generators.protocol + +import software.amazon.smithy.model.knowledge.OperationIndex +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.protocoltests.traits.AppliesTo +import software.amazon.smithy.protocoltests.traits.HttpMalformedRequestTestCase +import software.amazon.smithy.protocoltests.traits.HttpMalformedRequestTestsTrait +import software.amazon.smithy.protocoltests.traits.HttpRequestTestCase +import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait +import software.amazon.smithy.protocoltests.traits.HttpResponseTestCase +import software.amazon.smithy.protocoltests.traits.HttpResponseTestsTrait +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.allow +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.shouldPanic +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.RustModule +import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.docs +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock +import software.amazon.smithy.rust.codegen.core.rustlang.rustInlineTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.withBlock +import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.testutil.testDependenciesOnly +import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.core.util.getTrait +import software.amazon.smithy.rust.codegen.core.util.orNull +import software.amazon.smithy.rust.codegen.core.util.outputShape +import software.amazon.smithy.rust.codegen.core.util.toSnakeCase +import java.util.logging.Logger + +/** + * Common interface to generate protocol tests for a given [operationShape]. + */ +abstract class ProtocolTestGenerator { + abstract val codegenContext: CodegenContext + abstract val protocolSupport: ProtocolSupport + abstract val operationShape: OperationShape + abstract val appliesTo: AppliesTo + abstract val logger: Logger + + /** + * We expect these tests to fail due to shortcomings in our implementation. + * They will _fail_ if they pass, so we will discover and remove them if we fix them by accident. + **/ + abstract val expectFail: Set + + /** Only generate these tests; useful to temporarily set and shorten development cycles */ + abstract val runOnly: Set + + /** + * These tests are not even attempted to be generated, either because they will not compile + * or because they are flaky. + */ + abstract val disabledTests: Set + + /** The Rust module in which we should generate the protocol tests for [operationShape]. */ + private fun protocolTestsModule(): RustModule.LeafModule { + val operationName = codegenContext.symbolProvider.toSymbol(operationShape).name + val testModuleName = "${operationName.toSnakeCase()}_test" + val additionalAttributes = + listOf(Attribute(allow("unreachable_code", "unused_variables"))) + return RustModule.inlineTests(testModuleName, additionalAttributes = additionalAttributes) + } + + /** The entry point to render the protocol tests, invoked by the code generators. */ + fun render(writer: RustWriter) { + val allTests = allMatchingTestCases().fixBroken() + if (allTests.isEmpty()) { + return + } + + writer.withInlineModule(protocolTestsModule(), null) { + renderAllTestCases(allTests) + } + } + + /** Implementors should describe how to render the test cases. **/ + abstract fun RustWriter.renderAllTestCases(allTests: List) + + /** + * This function applies a "fix function" to each broken test before we synthesize it. + * Broken tests are those whose definitions in the `awslabs/smithy` repository are wrong. + * We try to contribute fixes upstream to pare down this function to the identity function. + */ + open fun List.fixBroken(): List = this + + /** Filter out test cases that are disabled or don't match the service protocol. */ + private fun List.filterMatching(): List = + if (runOnly.isEmpty()) { + this.filter { testCase -> testCase.protocol == codegenContext.protocol && !disabledTests.contains(testCase.id) } + } else { + logger.warning("Generating only specified tests") + this.filter { testCase -> runOnly.contains(testCase.id) } + } + + /** Do we expect this [testCase] to fail? */ + private fun expectFail(testCase: TestCase): Boolean = + expectFail.find { + it.id == testCase.id && it.kind == testCase.kind && it.service == codegenContext.serviceShape.id.toString() + } != null + + fun requestTestCases(): List { + val requestTests = + operationShape.getTrait()?.getTestCasesFor(appliesTo).orEmpty() + .map { TestCase.RequestTest(it) } + return requestTests.filterMatching() + } + + fun responseTestCases(): List { + val operationIndex = OperationIndex.of(codegenContext.model) + val outputShape = operationShape.outputShape(codegenContext.model) + + // `@httpResponseTests` trait can apply to operation shapes and structure shapes with the `@error` trait. + // Find both kinds for the operation for which we're generating protocol tests. + val responseTestsOnOperations = + operationShape.getTrait() + ?.getTestCasesFor(appliesTo).orEmpty().map { TestCase.ResponseTest(it, outputShape) } + val responseTestsOnErrors = + operationIndex.getErrors(operationShape).flatMap { error -> + error.getTrait() + ?.getTestCasesFor(appliesTo).orEmpty().map { TestCase.ResponseTest(it, error) } + } + + return (responseTestsOnOperations + responseTestsOnErrors).filterMatching() + } + + fun malformedRequestTestCases(): List { + // `@httpMalformedRequestTests` only make sense for servers. + val malformedRequestTests = + if (appliesTo == AppliesTo.SERVER) { + operationShape.getTrait() + ?.testCases.orEmpty().map { TestCase.MalformedRequestTest(it) } + } else { + emptyList() + } + return malformedRequestTests.filterMatching() + } + + /** + * Parses from the model and returns all test cases for [operationShape] applying to the [appliesTo] artifact type + * that should be rendered by implementors. + **/ + fun allMatchingTestCases(): List = + // Note there's no `@httpMalformedResponseTests`: https://github.com/smithy-lang/smithy/issues/2334 + requestTestCases() + responseTestCases() + malformedRequestTestCases() + + fun renderTestCaseBlock( + testCase: TestCase, + testModuleWriter: RustWriter, + block: Writable, + ) { + if (testCase.documentation != null) { + testModuleWriter.docs(testCase.documentation!!, templating = false) + } + testModuleWriter.docs("Test ID: ${testCase.id}") + + // The `#[traced_test]` macro desugars to using `tracing`, so we need to depend on the latter explicitly in + // case the code rendered by the test does not make use of `tracing` at all. + val tracingDevDependency = testDependenciesOnly { addDependency(CargoDependency.Tracing.toDevDependency()) } + testModuleWriter.rustInlineTemplate("#{TracingDevDependency:W}", "TracingDevDependency" to tracingDevDependency) + Attribute.TokioTest.render(testModuleWriter) + Attribute.TracedTest.render(testModuleWriter) + + if (expectFail(testCase)) { + shouldPanic().render(testModuleWriter) + } + val fnNameSuffix = + when (testCase) { + is TestCase.ResponseTest -> "_response" + is TestCase.RequestTest -> "_request" + is TestCase.MalformedRequestTest -> "_malformed_request" + } + testModuleWriter.rustBlock("async fn ${testCase.id.toSnakeCase()}$fnNameSuffix()") { + block(this) + } + } + + fun checkRequiredHeaders( + rustWriter: RustWriter, + actualExpression: String, + requireHeaders: List, + ) { + basicCheck( + requireHeaders, + rustWriter, + "required_headers", + actualExpression, + "require_headers", + ) + } + + fun checkForbidHeaders( + rustWriter: RustWriter, + actualExpression: String, + forbidHeaders: List, + ) { + basicCheck( + forbidHeaders, + rustWriter, + "forbidden_headers", + actualExpression, + "forbid_headers", + ) + } + + fun checkHeaders( + rustWriter: RustWriter, + actualExpression: String, + headers: Map, + ) { + if (headers.isEmpty()) { + return + } + val variableName = "expected_headers" + rustWriter.withBlock("let $variableName = [", "];") { + writeWithNoFormatting( + headers.entries.joinToString(",") { + "(${it.key.dq()}, ${it.value.dq()})" + }, + ) + } + assertOk(rustWriter) { + write( + "#T($actualExpression, $variableName)", + RuntimeType.protocolTest(codegenContext.runtimeConfig, "validate_headers"), + ) + } + } + + fun basicCheck( + params: List, + rustWriter: RustWriter, + expectedVariableName: String, + actualExpression: String, + checkFunction: String, + ) { + if (params.isEmpty()) { + return + } + rustWriter.withBlock("let $expectedVariableName = ", ";") { + strSlice(this, params) + } + assertOk(rustWriter) { + rustWriter.rust( + "#T($actualExpression, $expectedVariableName)", + RuntimeType.protocolTest(codegenContext.runtimeConfig, checkFunction), + ) + } + } + + /** + * Wraps `inner` in a call to `aws_smithy_protocol_test::assert_ok`, a convenience wrapper + * for pretty printing protocol test helper results. + */ + fun assertOk( + rustWriter: RustWriter, + inner: Writable, + ) { + rustWriter.rust("#T(", RuntimeType.protocolTest(codegenContext.runtimeConfig, "assert_ok")) + inner(rustWriter) + rustWriter.write(");") + } + + private fun strSlice( + writer: RustWriter, + args: List, + ) { + writer.withBlock("&[", "]") { + rust(args.joinToString(",") { it.dq() }) + } + } +} + +/** + * Service shape IDs in common protocol test suites defined upstream. + */ +object ServiceShapeId { + const val AWS_JSON_10 = "aws.protocoltests.json10#JsonRpc10" + const val AWS_JSON_11 = "aws.protocoltests.json#JsonProtocol" + const val REST_JSON = "aws.protocoltests.restjson#RestJson" + const val REST_JSON_VALIDATION = "aws.protocoltests.restjson.validation#RestJsonValidation" +} + +data class FailingTest(val service: String, val id: String, val kind: TestCaseKind) + +sealed class TestCaseKind { + data object Request : TestCaseKind() + + data object Response : TestCaseKind() + + data object MalformedRequest : TestCaseKind() +} + +sealed class TestCase { + data class RequestTest(val testCase: HttpRequestTestCase) : TestCase() + + data class ResponseTest(val testCase: HttpResponseTestCase, val targetShape: StructureShape) : TestCase() + + data class MalformedRequestTest(val testCase: HttpMalformedRequestTestCase) : TestCase() + + /* + * `HttpRequestTestCase` and `HttpResponseTestCase` both implement `HttpMessageTestCase`, but + * `HttpMalformedRequestTestCase` doesn't, so we have to define the following trivial delegators to provide a nice + * common accessor API. + */ + + val id: String + get() = + when (this) { + is RequestTest -> this.testCase.id + is MalformedRequestTest -> this.testCase.id + is ResponseTest -> this.testCase.id + } + + val protocol: ShapeId + get() = + when (this) { + is RequestTest -> this.testCase.protocol + is MalformedRequestTest -> this.testCase.protocol + is ResponseTest -> this.testCase.protocol + } + + val kind: TestCaseKind + get() = + when (this) { + is RequestTest -> TestCaseKind.Request + is ResponseTest -> TestCaseKind.Response + is MalformedRequestTest -> TestCaseKind.MalformedRequest + } + + val documentation: String? + get() = + when (this) { + is RequestTest -> this.testCase.documentation.orNull() + is ResponseTest -> this.testCase.documentation.orNull() + is MalformedRequestTest -> this.testCase.documentation.orNull() + } +} diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt index 19ea83426a..5c10b5fddf 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt @@ -17,6 +17,7 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.ErrorTrait +import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProviderConfig @@ -222,7 +223,10 @@ class PythonServerCodegenVisitor( } } - override fun protocolTests() { + override fun protocolTestsForOperation( + writer: RustWriter, + operationShape: OperationShape, + ) { logger.warning("[python-server-codegen] Protocol tests are disabled for this language") } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt index 87b5506616..fd7b427a5f 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt @@ -610,12 +610,11 @@ open class ServerCodegenVisitor( /** * Generate protocol tests. This method can be overridden by other languages such has Python. */ - open fun protocolTests() { - rustCrate.withModule(ServerRustModule.Operation) { - ServerProtocolTestGenerator(codegenContext, protocolGeneratorFactory.support(), protocolGenerator).render( - this, - ) - } + open fun protocolTestsForOperation( + writer: RustWriter, + shape: OperationShape, + ) { + ServerProtocolTestGenerator(codegenContext, protocolGeneratorFactory.support(), shape).render(writer) } /** @@ -648,9 +647,6 @@ open class ServerCodegenVisitor( ServerRuntimeTypesReExportsGenerator(codegenContext).render(this) } - // Generate protocol tests. - protocolTests() - // Generate service module. rustCrate.withModule(ServerRustModule.Service) { ServerServiceGenerator( @@ -693,6 +689,11 @@ open class ServerCodegenVisitor( codegenDecorator.postprocessOperationGenerateAdditionalStructures(shape) .forEach { structureShape -> this.structureShape(structureShape) } + + // Generate protocol tests. + rustCrate.withModule(ServerRustModule.Operation) { + protocolTestsForOperation(this, shape) + } } override fun blobShape(shape: BlobShape) { diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt index e06c81eb4d..194fa3eb2c 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt @@ -6,31 +6,21 @@ package software.amazon.smithy.rust.codegen.server.smithy.generators.protocol import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.node.Node import software.amazon.smithy.model.shapes.DoubleShape import software.amazon.smithy.model.shapes.FloatShape import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.traits.ErrorTrait import software.amazon.smithy.protocoltests.traits.AppliesTo import software.amazon.smithy.protocoltests.traits.HttpMalformedRequestTestCase -import software.amazon.smithy.protocoltests.traits.HttpMalformedRequestTestsTrait import software.amazon.smithy.protocoltests.traits.HttpMalformedResponseBodyDefinition import software.amazon.smithy.protocoltests.traits.HttpMalformedResponseDefinition import software.amazon.smithy.protocoltests.traits.HttpRequestTestCase -import software.amazon.smithy.protocoltests.traits.HttpRequestTestsTrait import software.amazon.smithy.protocoltests.traits.HttpResponseTestCase -import software.amazon.smithy.protocoltests.traits.HttpResponseTestsTrait -import software.amazon.smithy.rust.codegen.core.rustlang.Attribute -import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.allow -import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata -import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter -import software.amazon.smithy.rust.codegen.core.rustlang.Visibility import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.escape import software.amazon.smithy.rust.codegen.core.rustlang.rust @@ -40,10 +30,17 @@ import software.amazon.smithy.rust.codegen.core.rustlang.withBlock import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.FailingTest import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolSupport +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ProtocolTestGenerator +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId.AWS_JSON_10 +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId.AWS_JSON_11 +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId.REST_JSON +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.ServiceShapeId.REST_JSON_VALIDATION +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.TestCase +import software.amazon.smithy.rust.codegen.core.smithy.generators.protocol.TestCaseKind import software.amazon.smithy.rust.codegen.core.smithy.transformers.allErrors import software.amazon.smithy.rust.codegen.core.util.dq -import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rust.codegen.core.util.hasStreamingMember import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.inputShape @@ -53,24 +50,181 @@ import software.amazon.smithy.rust.codegen.core.util.outputShape import software.amazon.smithy.rust.codegen.core.util.toPascalCase import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency -import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerInstantiator import java.util.logging.Logger import kotlin.reflect.KFunction1 /** - * Generate protocol tests for an operation + * Generate server protocol tests for an [operationShape]. */ class ServerProtocolTestGenerator( - private val codegenContext: CodegenContext, - private val protocolSupport: ProtocolSupport, - private val protocolGenerator: ServerProtocolGenerator, -) { - private val logger = Logger.getLogger(javaClass.name) + override val codegenContext: CodegenContext, + override val protocolSupport: ProtocolSupport, + override val operationShape: OperationShape, +) : ProtocolTestGenerator() { + companion object { + private val ExpectFail: Set = + setOf( + // Endpoint trait is not implemented yet, see https://github.com/smithy-lang/smithy-rs/issues/950. + FailingTest(REST_JSON, "RestJsonEndpointTrait", TestCaseKind.Request), + FailingTest(REST_JSON, "RestJsonEndpointTraitWithHostLabel", TestCaseKind.Request), + FailingTest(REST_JSON, "RestJsonOmitsEmptyListQueryValues", TestCaseKind.Request), + // TODO(https://github.com/smithy-lang/smithy/pull/2315): Can be deleted when fixed tests are consumed in next Smithy version + FailingTest(REST_JSON, "RestJsonEnumPayloadRequest", TestCaseKind.Request), + FailingTest(REST_JSON, "RestJsonStringPayloadRequest", TestCaseKind.Request), + // Tests involving `@range` on floats. + // Pending resolution from the Smithy team, see https://github.com/smithy-lang/smithy-rs/issues/2007. + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloat_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloat_case1", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeMaxFloat", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeMinFloat", TestCaseKind.MalformedRequest), + // Tests involving floating point shapes and the `@range` trait; see https://github.com/smithy-lang/smithy-rs/issues/2007 + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloatOverride_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloatOverride_case1", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeMaxFloatOverride", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeMinFloatOverride", TestCaseKind.MalformedRequest), + // Some tests for the S3 service (restXml). + FailingTest("com.amazonaws.s3#AmazonS3", "GetBucketLocationUnwrappedOutput", TestCaseKind.Response), + FailingTest("com.amazonaws.s3#AmazonS3", "S3DefaultAddressing", TestCaseKind.Request), + FailingTest("com.amazonaws.s3#AmazonS3", "S3VirtualHostAddressing", TestCaseKind.Request), + FailingTest("com.amazonaws.s3#AmazonS3", "S3PathAddressing", TestCaseKind.Request), + FailingTest("com.amazonaws.s3#AmazonS3", "S3VirtualHostDualstackAddressing", TestCaseKind.Request), + FailingTest("com.amazonaws.s3#AmazonS3", "S3VirtualHostAccelerateAddressing", TestCaseKind.Request), + FailingTest("com.amazonaws.s3#AmazonS3", "S3VirtualHostDualstackAccelerateAddressing", TestCaseKind.Request), + FailingTest("com.amazonaws.s3#AmazonS3", "S3OperationAddressingPreferred", TestCaseKind.Request), + FailingTest("com.amazonaws.s3#AmazonS3", "S3OperationNoErrorWrappingResponse", TestCaseKind.Response), + // AwsJson1.0 failing tests. + FailingTest("aws.protocoltests.json10#JsonRpc10", "AwsJson10EndpointTraitWithHostLabel", TestCaseKind.Request), + FailingTest("aws.protocoltests.json10#JsonRpc10", "AwsJson10EndpointTrait", TestCaseKind.Request), + // AwsJson1.1 failing tests. + FailingTest(AWS_JSON_11, "AwsJson11EndpointTraitWithHostLabel", TestCaseKind.Request), + FailingTest(AWS_JSON_11, "AwsJson11EndpointTrait", TestCaseKind.Request), + FailingTest(AWS_JSON_11, "parses_the_request_id_from_the_response", TestCaseKind.Response), + // TODO(https://github.com/awslabs/smithy/issues/1683): This has been marked as failing until resolution of said issue + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsBlobList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsBooleanList_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsBooleanList_case1", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsStringList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsByteList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsShortList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsIntegerList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsLongList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsTimestampList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsDateTimeList", TestCaseKind.MalformedRequest), + FailingTest( + REST_JSON_VALIDATION, + "RestJsonMalformedUniqueItemsHttpDateList_case0", + TestCaseKind.MalformedRequest, + ), + FailingTest( + REST_JSON_VALIDATION, + "RestJsonMalformedUniqueItemsHttpDateList_case1", + TestCaseKind.MalformedRequest, + ), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsEnumList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsIntEnumList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsListList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsStructureList", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsUnionList_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsUnionList_case1", TestCaseKind.MalformedRequest), + // TODO(https://github.com/smithy-lang/smithy-rs/issues/2472): We don't respect the `@internal` trait + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumList_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumList_case1", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumMapKey_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumMapKey_case1", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumMapValue_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumMapValue_case1", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumString_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumString_case1", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumUnion_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumUnion_case1", TestCaseKind.MalformedRequest), + // TODO(https://github.com/awslabs/smithy/issues/1737): Specs on @internal, @tags, and enum values need to be clarified + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumTraitString_case0", TestCaseKind.MalformedRequest), + FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumTraitString_case1", TestCaseKind.MalformedRequest), + // These tests are broken because they are missing a target header. + FailingTest(AWS_JSON_10, "AwsJson10ServerPopulatesNestedDefaultsWhenMissingInRequestBody", TestCaseKind.Request), + FailingTest(AWS_JSON_10, "AwsJson10ServerPopulatesDefaultsWhenMissingInRequestBody", TestCaseKind.Request), + // Response defaults are not set when builders are not used https://github.com/smithy-lang/smithy-rs/issues/3339 + FailingTest(AWS_JSON_10, "AwsJson10ServerPopulatesDefaultsInResponseWhenMissingInParams", TestCaseKind.Response), + FailingTest(AWS_JSON_10, "AwsJson10ServerPopulatesNestedDefaultValuesWhenMissingInInResponseParams", TestCaseKind.Response), + ) + + private val DisabledTests = + setOf( + // TODO(https://github.com/smithy-lang/smithy-rs/issues/2891): Implement support for `@requestCompression` + "SDKAppendedGzipAfterProvidedEncoding_restJson1", + "SDKAppendedGzipAfterProvidedEncoding_restXml", + "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsJson1_0", + "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsJson1_1", + "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsQuery", + "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_ec2Query", + "SDKAppliedContentEncoding_awsJson1_0", + "SDKAppliedContentEncoding_awsJson1_1", + "SDKAppliedContentEncoding_awsQuery", + "SDKAppliedContentEncoding_ec2Query", + "SDKAppliedContentEncoding_restJson1", + "SDKAppliedContentEncoding_restXml", + // RestXml S3 tests that fail to compile + "S3EscapeObjectKeyInUriLabel", + "S3EscapePathObjectKeyInUriLabel", + "S3PreservesLeadingDotSegmentInUriLabel", + "S3PreservesEmbeddedDotSegmentInUriLabel", + ) + + // TODO(https://github.com/awslabs/smithy/issues/1506) + private fun fixRestJsonMalformedPatternReDOSString( + testCase: HttpMalformedRequestTestCase, + ): HttpMalformedRequestTestCase { + val brokenResponse = testCase.response + val brokenBody = brokenResponse.body.get() + val fixedBody = + HttpMalformedResponseBodyDefinition.builder() + .mediaType(brokenBody.mediaType) + .contents( + """ + { + "message" : "1 validation error detected. Value at '/evilString' failed to satisfy constraint: Member must satisfy regular expression pattern: ^([0-9]+)+${'$'}", + "fieldList" : [{"message": "Value at '/evilString' failed to satisfy constraint: Member must satisfy regular expression pattern: ^([0-9]+)+${'$'}", "path": "/evilString"}] + } + """.trimIndent(), + ) + .build() + + return testCase.toBuilder() + .response(brokenResponse.toBuilder().body(fixedBody).build()) + .build() + } + + // TODO(https://github.com/smithy-lang/smithy-rs/issues/1288): Move the fixed versions into + // `rest-json-extras.smithy` and put the unfixed ones in `ExpectFail`: this has the + // advantage that once our upstream PRs get merged and we upgrade to the next Smithy release, our build will + // fail and we will take notice to remove the fixes from `rest-json-extras.smithy`. This is exactly what the + // client does. + private val BrokenMalformedRequestTests: + Map, KFunction1> = + // TODO(https://github.com/awslabs/smithy/issues/1506) + mapOf( + Pair( + REST_JSON_VALIDATION, + "RestJsonMalformedPatternReDOSString", + ) to ::fixRestJsonMalformedPatternReDOSString, + ) + } + + override val appliesTo: AppliesTo + get() = AppliesTo.SERVER + override val expectFail: Set + get() = ExpectFail + override val runOnly: Set + get() = emptySet() + override val disabledTests: Set + get() = DisabledTests + + override val logger: Logger = Logger.getLogger(javaClass.name) private val model = codegenContext.model private val symbolProvider = codegenContext.symbolProvider - private val operationIndex = OperationIndex.of(codegenContext.model) + private val operationSymbol = symbolProvider.toSymbol(operationShape) private val serviceName = codegenContext.serviceShape.id.name.toPascalCase() private val operations = @@ -101,154 +255,30 @@ class ServerProtocolTestGenerator( private val codegenScope = arrayOf( "Bytes" to RuntimeType.Bytes, - "SmithyHttp" to RuntimeType.smithyHttp(codegenContext.runtimeConfig), - "Http" to RuntimeType.Http, "Hyper" to RuntimeType.Hyper, "Tokio" to ServerCargoDependency.TokioDev.toType(), "Tower" to RuntimeType.Tower, "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(codegenContext.runtimeConfig).toType(), "AssertEq" to RuntimeType.PrettyAssertions.resolve("assert_eq!"), - "Router" to ServerRuntimeType.router(codegenContext.runtimeConfig), ) - sealed class TestCase { - abstract val id: String - abstract val documentation: String? - abstract val protocol: ShapeId - abstract val testType: TestType - - data class RequestTest(val testCase: HttpRequestTestCase, val operationShape: OperationShape) : TestCase() { - override val id: String = testCase.id - override val documentation: String? = testCase.documentation.orNull() - override val protocol: ShapeId = testCase.protocol - override val testType: TestType = TestType.Request - } - - data class ResponseTest(val testCase: HttpResponseTestCase, val targetShape: StructureShape) : TestCase() { - override val id: String = testCase.id - override val documentation: String? = testCase.documentation.orNull() - override val protocol: ShapeId = testCase.protocol - override val testType: TestType = TestType.Response - } - - data class MalformedRequestTest(val testCase: HttpMalformedRequestTestCase) : TestCase() { - override val id: String = testCase.id - override val documentation: String? = testCase.documentation.orNull() - override val protocol: ShapeId = testCase.protocol - override val testType: TestType = TestType.MalformedRequest - } - } - - fun render(writer: RustWriter) { - for (operation in operations) { - renderOperationTestCases(operation, writer) - } - } - - private fun renderOperationTestCases( - operationShape: OperationShape, - writer: RustWriter, - ) { - val outputShape = operationShape.outputShape(codegenContext.model) - val operationSymbol = symbolProvider.toSymbol(operationShape) - - val requestTests = - operationShape.getTrait() - ?.getTestCasesFor(AppliesTo.SERVER).orEmpty().map { TestCase.RequestTest(it, operationShape) } - val responseTests = - operationShape.getTrait() - ?.getTestCasesFor(AppliesTo.SERVER).orEmpty().map { TestCase.ResponseTest(it, outputShape) } - val errorTests = - operationIndex.getErrors(operationShape).flatMap { error -> - val testCases = - error.getTrait() - ?.getTestCasesFor(AppliesTo.SERVER).orEmpty() - testCases.map { TestCase.ResponseTest(it, error) } - } - val malformedRequestTests = - operationShape.getTrait() - ?.testCases.orEmpty().map { TestCase.MalformedRequestTest(it) } - val allTests: List = - (requestTests + responseTests + errorTests + malformedRequestTests) - .filterMatching() - .fixBroken() - - if (allTests.isNotEmpty()) { - val operationName = operationSymbol.name - val module = - RustModule.LeafModule( - "server_${operationName.toSnakeCase()}_test", - RustMetadata( - additionalAttributes = - listOf( - Attribute.CfgTest, - Attribute(allow("unreachable_code", "unused_variables")), - ), - visibility = Visibility.PRIVATE, - ), - inline = true, - ) - writer.withInlineModule(module, null) { - renderAllTestCases(operationShape, allTests) - } - } - } - - private fun RustWriter.renderAllTestCases( - operationShape: OperationShape, - allTests: List, - ) { - allTests.forEach { - val operationSymbol = symbolProvider.toSymbol(operationShape) + override fun RustWriter.renderAllTestCases(allTests: List) { + for (it in allTests) { renderTestCaseBlock(it, this) { when (it) { - is TestCase.RequestTest -> - this.renderHttpRequestTestCase( - it.testCase, - operationShape, - operationSymbol, - ) - - is TestCase.ResponseTest -> - this.renderHttpResponseTestCase( - it.testCase, - it.targetShape, - operationShape, - operationSymbol, - ) - - is TestCase.MalformedRequestTest -> - this.renderHttpMalformedRequestTestCase( - it.testCase, - operationShape, - operationSymbol, - ) + is TestCase.RequestTest -> this.renderHttpRequestTestCase(it.testCase) + is TestCase.ResponseTest -> this.renderHttpResponseTestCase(it.testCase, it.targetShape) + is TestCase.MalformedRequestTest -> this.renderHttpMalformedRequestTestCase(it.testCase) } } } } - private fun OperationShape.toName(): String = - RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(this).name.toSnakeCase()) - /** - * Filter out test cases that are disabled or don't match the service protocol + * Broken tests in the `awslabs/smithy` repository are usually wrong because they have not been written + * with a server-side perspective in mind. */ - private fun List.filterMatching(): List { - return if (RunOnly.isNullOrEmpty()) { - this.filter { testCase -> - testCase.protocol == codegenContext.protocol && - !DisableTests.contains(testCase.id) - } - } else { - this.filter { RunOnly.contains(it.id) } - } - } - - // This function applies a "fix function" to each broken test before we synthesize it. - // Broken tests are those whose definitions in the `awslabs/smithy` repository are wrong, usually because they have - // not been written with a server-side perspective in mind. - private fun List.fixBroken(): List = + override fun List.fixBroken(): List = this.map { when (it) { is TestCase.MalformedRequestTest -> { @@ -264,45 +294,12 @@ class ServerProtocolTestGenerator( } } - private fun renderTestCaseBlock( - testCase: TestCase, - testModuleWriter: RustWriter, - block: Writable, - ) { - testModuleWriter.newlinePrefix = "/// " - if (testCase.documentation != null) { - testModuleWriter.writeWithNoFormatting(testCase.documentation) - } - - testModuleWriter.rust("Test ID: ${testCase.id}") - testModuleWriter.newlinePrefix = "" - - Attribute.TokioTest.render(testModuleWriter) - - if (expectFail(testCase)) { - testModuleWriter.writeWithNoFormatting("#[should_panic]") - } - val fnNameSuffix = - when (testCase.testType) { - is TestType.Response -> "_response" - is TestType.Request -> "_request" - is TestType.MalformedRequest -> "_malformed_request" - } - testModuleWriter.rustBlock("async fn ${testCase.id.toSnakeCase()}$fnNameSuffix()") { - block(this) - } - } - /** * Renders an HTTP request test case. * We are given an HTTP request in the test case, and we assert that when we deserialize said HTTP request into * an operation's input shape, the resulting shape is of the form we expect, as defined in the test case. */ - private fun RustWriter.renderHttpRequestTestCase( - httpRequestTestCase: HttpRequestTestCase, - operationShape: OperationShape, - operationSymbol: Symbol, - ) { + private fun RustWriter.renderHttpRequestTestCase(httpRequestTestCase: HttpRequestTestCase) { if (!protocolSupport.requestDeserialization) { rust("/* test case disabled for this protocol (not yet supported) */") return @@ -327,11 +324,6 @@ class ServerProtocolTestGenerator( } } - private fun expectFail(testCase: TestCase): Boolean = - ExpectFail.find { - it.id == testCase.id && it.testType == testCase.testType && it.service == codegenContext.serviceShape.id.toString() - } != null - /** * Renders an HTTP response test case. * We are given an operation output shape or an error shape in the `params` field, and we assert that when we @@ -341,8 +333,6 @@ class ServerProtocolTestGenerator( private fun RustWriter.renderHttpResponseTestCase( testCase: HttpResponseTestCase, shape: StructureShape, - operationShape: OperationShape, - operationSymbol: Symbol, ) { val operationErrorName = "crate::error::${operationSymbol.name}Error" @@ -375,11 +365,7 @@ class ServerProtocolTestGenerator( * We are given a request definition and a response definition, and we have to assert that the request is rejected * with the given response. */ - private fun RustWriter.renderHttpMalformedRequestTestCase( - testCase: HttpMalformedRequestTestCase, - operationShape: OperationShape, - operationSymbol: Symbol, - ) { + private fun RustWriter.renderHttpMalformedRequestTestCase(testCase: HttpMalformedRequestTestCase) { val (_, outputT) = operationInputOutputTypes[operationShape]!! val panicMessage = "request should have been rejected, but we accepted it; we parsed operation input `{:?}`" @@ -515,7 +501,7 @@ class ServerProtocolTestGenerator( private fun checkHandlerWasEntered(rustWriter: RustWriter) { rustWriter.rust( """ - assert!(receiver.recv().await.is_some()); + assert!(receiver.recv().await.is_some(), "we expected operation handler to be invoked but it was not entered"); """, ) } @@ -691,269 +677,4 @@ class ServerProtocolTestGenerator( *codegenScope, ) } - - private fun checkRequiredHeaders( - rustWriter: RustWriter, - actualExpression: String, - requireHeaders: List, - ) { - basicCheck( - requireHeaders, - rustWriter, - "required_headers", - actualExpression, - "require_headers", - ) - } - - private fun checkForbidHeaders( - rustWriter: RustWriter, - actualExpression: String, - forbidHeaders: List, - ) { - basicCheck( - forbidHeaders, - rustWriter, - "forbidden_headers", - actualExpression, - "forbid_headers", - ) - } - - private fun checkHeaders( - rustWriter: RustWriter, - actualExpression: String, - headers: Map, - ) { - if (headers.isEmpty()) { - return - } - val variableName = "expected_headers" - rustWriter.withBlock("let $variableName = [", "];") { - writeWithNoFormatting( - headers.entries.joinToString(",") { - "(${it.key.dq()}, ${it.value.dq()})" - }, - ) - } - assertOk(rustWriter) { - rust( - "#T($actualExpression, $variableName)", - RuntimeType.protocolTest(codegenContext.runtimeConfig, "validate_headers"), - ) - } - } - - private fun basicCheck( - params: List, - rustWriter: RustWriter, - expectedVariableName: String, - actualExpression: String, - checkFunction: String, - ) { - if (params.isEmpty()) { - return - } - rustWriter.withBlock("let $expectedVariableName = ", ";") { - strSlice(this, params) - } - assertOk(rustWriter) { - rustWriter.rust( - "#T($actualExpression, $expectedVariableName)", - RuntimeType.protocolTest(codegenContext.runtimeConfig, checkFunction), - ) - } - } - - /** - * wraps `inner` in a call to `aws_smithy_protocol_test::assert_ok`, a convenience wrapper - * for pretty printing protocol test helper results - */ - private fun assertOk( - rustWriter: RustWriter, - inner: Writable, - ) { - rustWriter.rust("#T(", RuntimeType.protocolTest(codegenContext.runtimeConfig, "assert_ok")) - inner(rustWriter) - rustWriter.write(");") - } - - private fun strSlice( - writer: RustWriter, - args: List, - ) { - writer.withBlock("&[", "]") { - rust(args.joinToString(",") { it.dq() }) - } - } - - companion object { - sealed class TestType { - object Request : TestType() - - object Response : TestType() - - object MalformedRequest : TestType() - } - - data class FailingTest(val service: String, val id: String, val testType: TestType) - - // These tests fail due to shortcomings in our implementation. - // These could be configured via runtime configuration, but since this won't be long-lasting, - // it makes sense to do the simplest thing for now. - // The test will _fail_ if these pass, so we will discover & remove if we fix them by accident - private const val AWS_JSON11 = "aws.protocoltests.json#JsonProtocol" - private const val AWS_JSON10 = "aws.protocoltests.json10#JsonRpc10" - private const val REST_JSON = "aws.protocoltests.restjson#RestJson" - private const val REST_JSON_VALIDATION = "aws.protocoltests.restjson.validation#RestJsonValidation" - private val ExpectFail: Set = - setOf( - // Endpoint trait is not implemented yet, see https://github.com/smithy-lang/smithy-rs/issues/950. - FailingTest(REST_JSON, "RestJsonEndpointTrait", TestType.Request), - FailingTest(REST_JSON, "RestJsonEndpointTraitWithHostLabel", TestType.Request), - FailingTest(REST_JSON, "RestJsonOmitsEmptyListQueryValues", TestType.Request), - // TODO(https://github.com/smithy-lang/smithy/pull/2315): Can be deleted when fixed tests are consumed in next Smithy version - FailingTest(REST_JSON, "RestJsonEnumPayloadRequest", TestType.Request), - FailingTest(REST_JSON, "RestJsonStringPayloadRequest", TestType.Request), - // Tests involving `@range` on floats. - // Pending resolution from the Smithy team, see https://github.com/smithy-lang/smithy-rs/issues/2007. - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloat_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloat_case1", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeMaxFloat", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeMinFloat", TestType.MalformedRequest), - // Tests involving floating point shapes and the `@range` trait; see https://github.com/smithy-lang/smithy-rs/issues/2007 - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloatOverride_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloatOverride_case1", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeMaxFloatOverride", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeMinFloatOverride", TestType.MalformedRequest), - // Some tests for the S3 service (restXml). - FailingTest("com.amazonaws.s3#AmazonS3", "GetBucketLocationUnwrappedOutput", TestType.Response), - FailingTest("com.amazonaws.s3#AmazonS3", "S3DefaultAddressing", TestType.Request), - FailingTest("com.amazonaws.s3#AmazonS3", "S3VirtualHostAddressing", TestType.Request), - FailingTest("com.amazonaws.s3#AmazonS3", "S3PathAddressing", TestType.Request), - FailingTest("com.amazonaws.s3#AmazonS3", "S3VirtualHostDualstackAddressing", TestType.Request), - FailingTest("com.amazonaws.s3#AmazonS3", "S3VirtualHostAccelerateAddressing", TestType.Request), - FailingTest("com.amazonaws.s3#AmazonS3", "S3VirtualHostDualstackAccelerateAddressing", TestType.Request), - FailingTest("com.amazonaws.s3#AmazonS3", "S3OperationAddressingPreferred", TestType.Request), - FailingTest("com.amazonaws.s3#AmazonS3", "S3OperationNoErrorWrappingResponse", TestType.Response), - // AwsJson1.0 failing tests. - FailingTest("aws.protocoltests.json10#JsonRpc10", "AwsJson10EndpointTraitWithHostLabel", TestType.Request), - FailingTest("aws.protocoltests.json10#JsonRpc10", "AwsJson10EndpointTrait", TestType.Request), - // AwsJson1.1 failing tests. - FailingTest(AWS_JSON11, "AwsJson11EndpointTraitWithHostLabel", TestType.Request), - FailingTest(AWS_JSON11, "AwsJson11EndpointTrait", TestType.Request), - FailingTest(AWS_JSON11, "parses_the_request_id_from_the_response", TestType.Response), - // TODO(https://github.com/awslabs/smithy/issues/1683): This has been marked as failing until resolution of said issue - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsBlobList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsBooleanList_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsBooleanList_case1", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsStringList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsByteList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsShortList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsIntegerList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsLongList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsTimestampList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsDateTimeList", TestType.MalformedRequest), - FailingTest( - REST_JSON_VALIDATION, - "RestJsonMalformedUniqueItemsHttpDateList_case0", - TestType.MalformedRequest, - ), - FailingTest( - REST_JSON_VALIDATION, - "RestJsonMalformedUniqueItemsHttpDateList_case1", - TestType.MalformedRequest, - ), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsEnumList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsIntEnumList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsListList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsStructureList", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsUnionList_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedUniqueItemsUnionList_case1", TestType.MalformedRequest), - // TODO(https://github.com/smithy-lang/smithy-rs/issues/2472): We don't respect the `@internal` trait - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumList_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumList_case1", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumMapKey_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumMapKey_case1", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumMapValue_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumMapValue_case1", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumString_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumString_case1", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumUnion_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumUnion_case1", TestType.MalformedRequest), - // TODO(https://github.com/awslabs/smithy/issues/1737): Specs on @internal, @tags, and enum values need to be clarified - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumTraitString_case0", TestType.MalformedRequest), - FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedEnumTraitString_case1", TestType.MalformedRequest), - // These tests are broken because they are missing a target header - FailingTest(AWS_JSON10, "AwsJson10ServerPopulatesNestedDefaultsWhenMissingInRequestBody", TestType.Request), - FailingTest(AWS_JSON10, "AwsJson10ServerPopulatesDefaultsWhenMissingInRequestBody", TestType.Request), - // Response defaults are not set when builders are not used https://github.com/smithy-lang/smithy-rs/issues/3339 - FailingTest(AWS_JSON10, "AwsJson10ServerPopulatesDefaultsInResponseWhenMissingInParams", TestType.Response), - FailingTest(AWS_JSON10, "AwsJson10ServerPopulatesNestedDefaultValuesWhenMissingInInResponseParams", TestType.Response), - ) - private val RunOnly: Set? = null - - // These tests are not even attempted to be generated, either because they will not compile - // or because they are flaky - private val DisableTests = - setOf( - // TODO(https://github.com/smithy-lang/smithy-rs/issues/2891): Implement support for `@requestCompression` - "SDKAppendedGzipAfterProvidedEncoding_restJson1", - "SDKAppendedGzipAfterProvidedEncoding_restXml", - "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsJson1_0", - "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsJson1_1", - "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsQuery", - "SDKAppendsGzipAndIgnoresHttpProvidedEncoding_ec2Query", - "SDKAppliedContentEncoding_awsJson1_0", - "SDKAppliedContentEncoding_awsJson1_1", - "SDKAppliedContentEncoding_awsQuery", - "SDKAppliedContentEncoding_ec2Query", - "SDKAppliedContentEncoding_restJson1", - "SDKAppliedContentEncoding_restXml", - // RestXml S3 tests that fail to compile - "S3EscapeObjectKeyInUriLabel", - "S3EscapePathObjectKeyInUriLabel", - "S3PreservesLeadingDotSegmentInUriLabel", - "S3PreservesEmbeddedDotSegmentInUriLabel", - ) - - // TODO(https://github.com/awslabs/smithy/issues/1506) - private fun fixRestJsonMalformedPatternReDOSString( - testCase: HttpMalformedRequestTestCase, - ): HttpMalformedRequestTestCase { - val brokenResponse = testCase.response - val brokenBody = brokenResponse.body.get() - val fixedBody = - HttpMalformedResponseBodyDefinition.builder() - .mediaType(brokenBody.mediaType) - .contents( - """ - { - "message" : "1 validation error detected. Value at '/evilString' failed to satisfy constraint: Member must satisfy regular expression pattern: ^([0-9]+)+${'$'}", - "fieldList" : [{"message": "Value at '/evilString' failed to satisfy constraint: Member must satisfy regular expression pattern: ^([0-9]+)+${'$'}", "path": "/evilString"}] - } - """.trimIndent(), - ) - .build() - - return testCase.toBuilder() - .response(brokenResponse.toBuilder().body(fixedBody).build()) - .build() - } - - // TODO(https://github.com/smithy-lang/smithy-rs/issues/1288): Move the fixed versions into - // `rest-json-extras.smithy` and put the unfixed ones in `ExpectFail`: this has the - // advantage that once our upstream PRs get merged and we upgrade to the next Smithy release, our build will - // fail and we will take notice to remove the fixes from `rest-json-extras.smithy`. This is exactly what the - // client does. - private val BrokenMalformedRequestTests: - Map, KFunction1> = - // TODO(https://github.com/awslabs/smithy/issues/1506) - mapOf( - Pair( - REST_JSON_VALIDATION, - "RestJsonMalformedPatternReDOSString", - ) to ::fixRestJsonMalformedPatternReDOSString, - ) - } }