Skip to content

Commit 507ebe8

Browse files
author
Zelda Hessler
authored
v2 Smoketest codegen (#3758)
This PR adds codegen for service-defined smoketests. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 1223f61 commit 507ebe8

File tree

4 files changed

+318
-0
lines changed

4 files changed

+318
-0
lines changed

aws/sdk-codegen/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ dependencies {
2727
implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion")
2828
implementation("software.amazon.smithy:smithy-rules-engine:$smithyVersion")
2929
implementation("software.amazon.smithy:smithy-aws-endpoints:$smithyVersion")
30+
implementation("software.amazon.smithy:smithy-smoke-test-traits:$smithyVersion")
31+
implementation("software.amazon.smithy:smithy-aws-smoke-test-model:$smithyVersion")
3032
}
3133

3234
java {

aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ val DECORATORS: List<ClientCodegenDecorator> =
6363
TokenProvidersDecorator(),
6464
ServiceEnvConfigDecorator(),
6565
HttpRequestCompressionDecorator(),
66+
SmokeTestsDecorator(),
6667
),
6768
// S3 needs `AwsErrorCodeClassifier` to handle an `InternalError` as a transient error. We need to customize
6869
// that behavior for S3 in a way that does not conflict with the globally applied `RetryClassifierDecorator`.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rustsdk
7+
8+
import software.amazon.smithy.aws.smoketests.model.AwsSmokeTestModel
9+
import software.amazon.smithy.model.Model
10+
import software.amazon.smithy.model.node.ObjectNode
11+
import software.amazon.smithy.model.shapes.MemberShape
12+
import software.amazon.smithy.model.shapes.OperationShape
13+
import software.amazon.smithy.model.shapes.StructureShape
14+
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
15+
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
16+
import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentClientGenerator
17+
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
18+
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.cfg
19+
import software.amazon.smithy.rust.codegen.core.rustlang.AttributeKind
20+
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
21+
import software.amazon.smithy.rust.codegen.core.rustlang.containerDocs
22+
import software.amazon.smithy.rust.codegen.core.rustlang.docs
23+
import software.amazon.smithy.rust.codegen.core.rustlang.rust
24+
import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock
25+
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
26+
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
27+
import software.amazon.smithy.rust.codegen.core.smithy.generators.BuilderGenerator
28+
import software.amazon.smithy.rust.codegen.core.smithy.generators.Instantiator
29+
import software.amazon.smithy.rust.codegen.core.smithy.generators.setterName
30+
import software.amazon.smithy.rust.codegen.core.testutil.integrationTest
31+
import software.amazon.smithy.rust.codegen.core.util.dq
32+
import software.amazon.smithy.rust.codegen.core.util.expectTrait
33+
import software.amazon.smithy.rust.codegen.core.util.inputShape
34+
import software.amazon.smithy.rust.codegen.core.util.orNull
35+
import software.amazon.smithy.rust.codegen.core.util.toSnakeCase
36+
import software.amazon.smithy.smoketests.traits.Expectation
37+
import software.amazon.smithy.smoketests.traits.SmokeTestCase
38+
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait
39+
import java.util.Optional
40+
import java.util.logging.Logger
41+
42+
class SmokeTestsDecorator : ClientCodegenDecorator {
43+
override val name: String = "SmokeTests"
44+
override val order: Byte = 0
45+
private val logger: Logger = Logger.getLogger(javaClass.name)
46+
47+
private fun isSmokeTestSupported(smokeTestCase: SmokeTestCase): Boolean {
48+
AwsSmokeTestModel.getAwsVendorParams(smokeTestCase)?.orNull()?.let { vendorParams ->
49+
if (vendorParams.sigv4aRegionSet.isPresent) {
50+
logger.warning("skipping smoketest `${smokeTestCase.id}` with unsupported vendorParam `sigv4aRegionSet`")
51+
return false
52+
}
53+
// TODO(https://github.com/smithy-lang/smithy-rs/issues/3776) Once Account ID routing is supported,
54+
// update the vendorParams setter and remove this check.
55+
if (vendorParams.useAccountIdRouting()) {
56+
logger.warning("skipping smoketest `${smokeTestCase.id}` with unsupported vendorParam `useAccountIdRouting`")
57+
return false
58+
}
59+
}
60+
AwsSmokeTestModel.getS3VendorParams(smokeTestCase)?.orNull()?.let { s3VendorParams ->
61+
if (s3VendorParams.useGlobalEndpoint()) {
62+
logger.warning("skipping smoketest `${smokeTestCase.id}` with unsupported vendorParam `useGlobalEndpoint`")
63+
return false
64+
}
65+
}
66+
67+
return true
68+
}
69+
70+
override fun extras(
71+
codegenContext: ClientCodegenContext,
72+
rustCrate: RustCrate,
73+
) {
74+
// Get all operations with smoke tests
75+
val smokeTestedOperations =
76+
codegenContext.model.getOperationShapesWithTrait(SmokeTestsTrait::class.java).toList()
77+
val supportedTests =
78+
smokeTestedOperations.map { operationShape ->
79+
// filter out unsupported smoke tests, logging a warning for each one.
80+
val testCases =
81+
operationShape.expectTrait<SmokeTestsTrait>().testCases.filter { smokeTestCase ->
82+
isSmokeTestSupported(smokeTestCase)
83+
}
84+
85+
operationShape to testCases
86+
}
87+
// filter out operations with no supported smoke tests
88+
.filter { (_, testCases) -> testCases.isNotEmpty() }
89+
// Return if there are no supported smoke tests across all operations
90+
if (supportedTests.isEmpty()) return
91+
92+
rustCrate.integrationTest("smoketests") {
93+
// Don't run the tests in this module unless `RUSTFLAGS="--cfg smoketests"` is passed.
94+
Attribute(cfg("smoketests")).render(this, AttributeKind.Inner)
95+
96+
containerDocs(
97+
"""
98+
The tests in this module run against live AWS services. As such,
99+
they are disabled by default. To enable them, run the tests with
100+
101+
```sh
102+
RUSTFLAGS="--cfg smoketests" cargo test.
103+
```""",
104+
)
105+
106+
val model = codegenContext.model
107+
val moduleUseName = codegenContext.moduleUseName()
108+
rust("use $moduleUseName::{ Client, config };")
109+
110+
for ((operationShape, testCases) in supportedTests) {
111+
val operationName = operationShape.id.name.toSnakeCase()
112+
val operationInput = operationShape.inputShape(model)
113+
114+
docs("Smoke tests for the `$operationName` operation")
115+
116+
for (testCase in testCases) {
117+
Attribute.TokioTest.render(this)
118+
this.rustBlock("async fn test_${testCase.id.toSnakeCase()}()") {
119+
val instantiator = SmokeTestsInstantiator(codegenContext)
120+
instantiator.renderConf(this, testCase)
121+
rust("let client = Client::from_conf(conf);")
122+
instantiator.renderInput(this, operationShape, operationInput, testCase.params)
123+
instantiator.renderExpectation(this, model, testCase.expectation)
124+
}
125+
}
126+
}
127+
}
128+
}
129+
}
130+
131+
class SmokeTestsBuilderKindBehavior(val codegenContext: CodegenContext) : Instantiator.BuilderKindBehavior {
132+
override fun hasFallibleBuilder(shape: StructureShape): Boolean =
133+
BuilderGenerator.hasFallibleBuilder(shape, codegenContext.symbolProvider)
134+
135+
override fun setterName(memberShape: MemberShape): String = memberShape.setterName()
136+
137+
override fun doesSetterTakeInOption(memberShape: MemberShape): Boolean = true
138+
}
139+
140+
class SmokeTestsInstantiator(private val codegenContext: ClientCodegenContext) : Instantiator(
141+
codegenContext.symbolProvider,
142+
codegenContext.model,
143+
codegenContext.runtimeConfig,
144+
SmokeTestsBuilderKindBehavior(codegenContext),
145+
) {
146+
fun renderConf(
147+
writer: RustWriter,
148+
testCase: SmokeTestCase,
149+
) {
150+
writer.rust("let conf = config::Builder::new()")
151+
writer.indent()
152+
writer.rust(".behavior_version(config::BehaviorVersion::latest())")
153+
154+
val vendorParams = AwsSmokeTestModel.getAwsVendorParams(testCase)
155+
vendorParams.orNull()?.let { params ->
156+
writer.rust(".region(config::Region::new(${params.region.dq()}))")
157+
writer.rust(".use_dual_stack(${params.useDualstack()})")
158+
writer.rust(".use_fips(${params.useFips()})")
159+
params.uri.orNull()?.let { writer.rust(".endpoint_url($it)") }
160+
}
161+
162+
val s3VendorParams = AwsSmokeTestModel.getS3VendorParams(testCase)
163+
s3VendorParams.orNull()?.let { params ->
164+
writer.rust(".accelerate_(${params.useAccelerate()})")
165+
writer.rust(".force_path_style_(${params.forcePathStyle()})")
166+
writer.rust(".use_arn_region(${params.useArnRegion()})")
167+
writer.rust(".disable_multi_region_access_points(${params.useMultiRegionAccessPoints().not()})")
168+
}
169+
170+
writer.rust(".build();")
171+
writer.dedent()
172+
}
173+
174+
fun renderInput(
175+
writer: RustWriter,
176+
operationShape: OperationShape,
177+
inputShape: StructureShape,
178+
data: Optional<ObjectNode>,
179+
headers: Map<String, String> = mapOf(),
180+
ctx: Ctx = Ctx(),
181+
) {
182+
val operationBuilderName =
183+
FluentClientGenerator.clientOperationFnName(operationShape, codegenContext.symbolProvider)
184+
185+
writer.rust("let res = client.$operationBuilderName()")
186+
writer.indent()
187+
data.orNull()?.let {
188+
renderStructureMembers(writer, inputShape, it, headers, ctx)
189+
}
190+
writer.rust(".send().await;")
191+
writer.dedent()
192+
}
193+
194+
fun renderExpectation(
195+
writer: RustWriter,
196+
model: Model,
197+
expectation: Expectation,
198+
) {
199+
if (expectation.isSuccess) {
200+
writer.rust("""res.expect("request should succeed");""")
201+
} else if (expectation.isFailure) {
202+
val expectedErrShape = expectation.failure.orNull()?.errorId?.orNull()
203+
println(expectedErrShape)
204+
if (expectedErrShape != null) {
205+
val failureShape = model.expectShape(expectedErrShape)
206+
val errName = codegenContext.symbolProvider.toSymbol(failureShape).name.toSnakeCase()
207+
writer.rust(
208+
"""
209+
let err = res.expect_err("request should fail");
210+
let err = err.into_service_error();
211+
assert!(err.is_$errName())
212+
""",
213+
)
214+
} else {
215+
writer.rust("""res.expect_err("request should fail");""")
216+
}
217+
}
218+
}
219+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rustsdk
7+
8+
import org.junit.jupiter.api.Test
9+
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
10+
11+
class SmokeTestsDecoratorTest {
12+
companion object {
13+
// Can't use the dollar sign in a multiline string with doing it like this.
14+
private const val PREFIX = "\$version: \"2\""
15+
val model =
16+
"""
17+
$PREFIX
18+
namespace test
19+
20+
use aws.api#service
21+
use smithy.test#smokeTests
22+
use aws.auth#sigv4
23+
use aws.protocols#restJson1
24+
use smithy.rules#endpointRuleSet
25+
26+
@service(sdkId: "dontcare")
27+
@restJson1
28+
@sigv4(name: "dontcare")
29+
@auth([sigv4])
30+
@endpointRuleSet({
31+
"version": "1.0",
32+
"rules": [{ "type": "endpoint", "conditions": [], "endpoint": { "url": "https://example.com" } }],
33+
"parameters": {
34+
"Region": { "required": false, "type": "String", "builtIn": "AWS::Region" },
35+
}
36+
})
37+
service TestService {
38+
version: "2023-01-01",
39+
operations: [SomeOperation]
40+
}
41+
42+
@smokeTests([
43+
{
44+
id: "SomeOperationSuccess",
45+
params: {}
46+
vendorParams: {
47+
region: "us-west-2"
48+
}
49+
expect: { success: {} }
50+
}
51+
{
52+
id: "SomeOperationFailure",
53+
params: {}
54+
vendorParams: {
55+
region: "us-west-2"
56+
}
57+
expect: { failure: {} }
58+
}
59+
{
60+
id: "SomeOperationFailureExplicitShape",
61+
params: {}
62+
vendorParams: {
63+
region: "us-west-2"
64+
}
65+
expect: {
66+
failure: { errorId: FooException }
67+
}
68+
}
69+
])
70+
@http(uri: "/SomeOperation", method: "POST")
71+
@optionalAuth
72+
operation SomeOperation {
73+
input: SomeInput,
74+
output: SomeOutput,
75+
errors: [FooException]
76+
}
77+
78+
@input
79+
structure SomeInput {}
80+
81+
@output
82+
structure SomeOutput {}
83+
84+
@error("server")
85+
structure FooException { }
86+
""".asSmithyModel()
87+
}
88+
89+
@Test
90+
fun smokeTestSdkCodegen() {
91+
awsSdkIntegrationTest(model) { _, _ ->
92+
// It should compile. We can't run the tests
93+
// because they don't target a real service.
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)