-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
577 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"id": "756754c3-f6e1-4ff2-ae31-08b3b67b6750", | ||
"type": "feature", | ||
"description": "Add support for [smoke tests](https://smithy.io/2.0/additional-specs/smoke-tests.html)" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
...otlin/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsIntegration.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package software.amazon.smithy.kotlin.codegen.rendering.smoketests | ||
|
||
import software.amazon.smithy.kotlin.codegen.KotlinSettings | ||
import software.amazon.smithy.kotlin.codegen.core.CodegenContext | ||
import software.amazon.smithy.kotlin.codegen.core.DEFAULT_TEST_SOURCE_SET_ROOT | ||
import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator | ||
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration | ||
import software.amazon.smithy.kotlin.codegen.model.hasTrait | ||
import software.amazon.smithy.kotlin.codegen.utils.topDownOperations | ||
import software.amazon.smithy.model.Model | ||
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait | ||
|
||
/** | ||
* Renders smoke test runner for a service if any of the operations have the [SmokeTestsTrait]. | ||
*/ | ||
class SmokeTestsIntegration : KotlinIntegration { | ||
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = | ||
model.topDownOperations(settings.service).any { it.hasTrait<SmokeTestsTrait>() } | ||
|
||
override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) = | ||
delegator.useFileWriter( | ||
"SmokeTests.kt", | ||
"${ctx.settings.pkg.name}.smoketests", | ||
DEFAULT_TEST_SOURCE_SET_ROOT, | ||
) { writer -> | ||
SmokeTestsRunnerGenerator( | ||
writer, | ||
ctx, | ||
).render() | ||
} | ||
} |
193 changes: 193 additions & 0 deletions
193
...n/software/amazon/smithy/kotlin/codegen/rendering/smoketests/SmokeTestsRunnerGenerator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
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.model.getTrait | ||
import software.amazon.smithy.kotlin.codegen.model.hasTrait | ||
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.topDownOperations | ||
import software.amazon.smithy.model.shapes.OperationShape | ||
import software.amazon.smithy.smoketests.traits.SmokeTestCase | ||
import software.amazon.smithy.smoketests.traits.SmokeTestsTrait | ||
import kotlin.jvm.optionals.getOrNull | ||
|
||
object SmokeTestsRunner : SectionId | ||
object SmokeTestAdditionalEnvVars : SectionId | ||
object SmokeTestDefaultConfig : SectionId | ||
object SmokeTestRegionDefault : SectionId | ||
object SmokeTestHttpEngineOverride : SectionId | ||
|
||
const val SKIP_TAGS = "AWS_SMOKE_TEST_SKIP_TAGS" | ||
const val SERVICE_FILTER = "AWS_SMOKE_TEST_SERVICE_IDS" | ||
|
||
/** | ||
* Renders smoke tests runner for a service | ||
*/ | ||
class SmokeTestsRunnerGenerator( | ||
private val writer: KotlinWriter, | ||
ctx: CodegenContext, | ||
) { | ||
private val model = ctx.model | ||
private val sdkId = ctx.settings.sdkId | ||
private val symbolProvider = ctx.symbolProvider | ||
private val service = symbolProvider.toSymbol(model.expectShape(ctx.settings.service)) | ||
private val operations = ctx.model.topDownOperations(ctx.settings.service).filter { it.hasTrait<SmokeTestsTrait>() } | ||
|
||
internal fun render() { | ||
writer.declareSection(SmokeTestsRunner) { | ||
write("private var exitCode = 0") | ||
write( | ||
"private val skipTags = #T.System.getenv(#S)?.let { it.split(#S).map { it.trim() }.toSet() } ?: emptySet()", | ||
RuntimeTypes.Core.Utils.PlatformProvider, | ||
SKIP_TAGS, | ||
",", | ||
) | ||
write( | ||
"private val serviceFilter = #T.System.getenv(#S)?.let { it.split(#S).map { it.trim() }.toSet() } ?: emptySet()", | ||
RuntimeTypes.Core.Utils.PlatformProvider, | ||
SERVICE_FILTER, | ||
",", | ||
) | ||
declareSection(SmokeTestAdditionalEnvVars) | ||
write("") | ||
withBlock("public suspend fun main() {", "}") { | ||
renderFunctionCalls() | ||
write("#T(exitCode)", RuntimeTypes.Core.SmokeTests.exitProcess) | ||
} | ||
write("") | ||
renderFunctions() | ||
} | ||
} | ||
|
||
private fun renderFunctionCalls() { | ||
operations.forEach { operation -> | ||
operation.getTrait<SmokeTestsTrait>()?.testCases?.forEach { testCase -> | ||
writer.write("${testCase.functionName}()") | ||
} | ||
} | ||
} | ||
|
||
private fun renderFunctions() { | ||
operations.forEach { operation -> | ||
operation.getTrait<SmokeTestsTrait>()?.testCases?.forEach { testCase -> | ||
renderFunction(operation, testCase) | ||
writer.write("") | ||
} | ||
} | ||
} | ||
|
||
private fun renderFunction(operation: OperationShape, testCase: SmokeTestCase) { | ||
writer.withBlock("private suspend fun ${testCase.functionName}() {", "}") { | ||
write("val tags = setOf<String>(${testCase.tags.joinToString(",") { it.dq()} })") | ||
writer.withBlock("if ((serviceFilter.isNotEmpty() && #S !in serviceFilter) || tags.any { it in skipTags }) {", "}", sdkId) { | ||
printTestResult( | ||
sdkId.filter { !it.isWhitespace() }, | ||
testCase.id, | ||
testCase.expectation.isFailure, | ||
writer, | ||
"ok", | ||
"# skip", | ||
) | ||
writer.write("return") | ||
} | ||
write("") | ||
withInlineBlock("try {", "} ") { | ||
renderClient(testCase) | ||
renderOperation(operation, testCase) | ||
} | ||
withBlock("catch (e: Exception) {", "}") { | ||
renderCatchBlock(testCase) | ||
} | ||
} | ||
} | ||
|
||
private fun renderClient(testCase: SmokeTestCase) { | ||
writer.withInlineBlock("#L {", "}", service) { | ||
if (testCase.vendorParams.isPresent) { | ||
testCase.vendorParams.get().members.forEach { vendorParam -> | ||
if (vendorParam.key.value == "region") { | ||
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) | ||
} | ||
declareSection(SmokeTestHttpEngineOverride) | ||
} | ||
} | ||
|
||
private fun renderOperation(operation: OperationShape, testCase: SmokeTestCase) { | ||
val operationSymbol = symbolProvider.toSymbol(model.getShape(operation.input.get()).get()) | ||
|
||
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()) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun renderCatchBlock(testCase: SmokeTestCase) { | ||
val expected = 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") | ||
printTestResult( | ||
sdkId.filter { !it.isWhitespace() }, | ||
testCase.id, | ||
testCase.expectation.isFailure, | ||
writer, | ||
) | ||
writer.write("if (!success) exitCode = 1") | ||
} | ||
|
||
/** | ||
* Tries to get the specific exception required in the failure criterion of a test. | ||
* If no specific exception is required we default to the generic smoke tests failure exception. | ||
*/ | ||
private fun getFailureCriterion(testCase: SmokeTestCase): Symbol = | ||
testCase.expectation.failure.getOrNull()?.errorId?.getOrNull()?.let { | ||
symbolProvider.toSymbol(model.getShape(it).get()) | ||
} ?: RuntimeTypes.HttpClient.Interceptors.SmokeTestsFailureException | ||
|
||
/** | ||
* Renders print statement for smoke test result in accordance to design doc & test anything protocol (TAP) | ||
*/ | ||
private fun printTestResult( | ||
service: String, | ||
testCase: String, | ||
errorExpected: Boolean, | ||
writer: KotlinWriter, | ||
statusOverride: String? = null, | ||
directive: String? = "", | ||
) { | ||
val expectation = if (errorExpected) "error expected from service" else "no error expected from service" | ||
val status = statusOverride ?: "\$status" | ||
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() |
27 changes: 27 additions & 0 deletions
27
...tlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/util/Node.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package software.amazon.smithy.kotlin.codegen.rendering.util | ||
|
||
import software.amazon.smithy.kotlin.codegen.utils.dq | ||
import software.amazon.smithy.model.node.ArrayNode | ||
import software.amazon.smithy.model.node.BooleanNode | ||
import software.amazon.smithy.model.node.Node | ||
import software.amazon.smithy.model.node.NullNode | ||
import software.amazon.smithy.model.node.NumberNode | ||
import software.amazon.smithy.model.node.ObjectNode | ||
import software.amazon.smithy.model.node.StringNode | ||
|
||
/** | ||
* Formats a [Node] into a String for codegen. | ||
*/ | ||
fun Node.format(): String = when (this) { | ||
is NullNode -> "null" | ||
is StringNode -> value.dq() | ||
is BooleanNode -> value.toString() | ||
is NumberNode -> value.toString() | ||
is ArrayNode -> elements.joinToString(",", "listOf(", ")") { element -> | ||
element.format() | ||
} | ||
is ObjectNode -> stringMap.entries.joinToString(", ", "mapOf(", ")") { (key, value) -> | ||
"${key.dq()} to ${value.format()}" | ||
} | ||
else -> throw Exception("Unexpected node type: $this") | ||
} |
14 changes: 14 additions & 0 deletions
14
...mithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/utils/Model.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package software.amazon.smithy.kotlin.codegen.utils | ||
|
||
import software.amazon.smithy.model.Model | ||
import software.amazon.smithy.model.knowledge.TopDownIndex | ||
import software.amazon.smithy.model.shapes.OperationShape | ||
import software.amazon.smithy.model.shapes.ShapeId | ||
|
||
/** | ||
* Syntactic sugar for getting a services operations | ||
*/ | ||
fun Model.topDownOperations(service: ShapeId): Set<OperationShape> { | ||
val topDownIndex = TopDownIndex.of(this) | ||
return topDownIndex.getContainedOperations(service) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.