Skip to content

feat: add protocol tests runner #1293

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/e9fa30cd-1aa7-4c5d-b01a-384078b8863a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": "e9fa30cd-1aa7-4c5d-b01a-384078b8863a",
"type": "feature",
"description": "Added a protocol tests runner, a new mechanism to run the tests and report the results in JSON format detached from the test phase of a regular build."
}
Comment on lines +1 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This update isn't user-facing so wouldn't typically need a changelog message. You can delete this and add the no-changelog label to this PR.

3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ apiValidation {
"testing",
"smithy-kotlin-codegen",
"smithy-kotlin-codegen-testutils",
"smithy-kotlin-protocol-tests-codegen",
"smithy-aws-kotlin-codegen",
"protocol-tests",
"protocol-tests-runner",
"protocol-tests-utils",
"aws-signing-benchmarks",
"channel-benchmarks",
"http-benchmarks",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {
}

fun execute() {
generateShapes()
generateNonShapes()
writers.flushWriters()
}

fun generateShapes() {
logger.info("Generating Kotlin client for service ${settings.service}")

logger.info("Walking shapes from ${settings.service} to find shapes to generate")
Expand All @@ -122,15 +128,7 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {
serviceShapes.forEach { it.accept(this) }

protocolGenerator?.apply {
val ctx = ProtocolGenerator.GenerationContext(
settings,
model,
service,
symbolProvider,
integrations,
protocol,
writers,
)
val ctx = generationContext()

logger.info("[${service.id}] Generating unit tests for protocol $protocol")
generateProtocolUnitTests(ctx)
Expand All @@ -144,7 +142,9 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {
logger.info("[${service.id}] Generating auth scheme provider for protocol $protocol")
generateAuthSchemeProvider(ctx)
}
}

fun generateNonShapes() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: The name of this method is not very clear because it's not obvious what "non-shapes" are. Can we come up with a more descriptive name that will make more sense in calling code? (e.g., generateIntegrations, generateAdditionalFiles, etc.)

writers.finalize()

if (settings.build.generateDefaultBuildFiles) {
Expand All @@ -156,10 +156,18 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {

// write files defined by integrations
integrations.forEach { it.writeAdditionalFiles(baseGenerationContext, writers) }

writers.flushWriters()
}

fun generationContext(): ProtocolGenerator.GenerationContext = ProtocolGenerator.GenerationContext(
settings,
model,
service,
symbolProvider,
integrations,
protocolGenerator!!.protocol,
writers,
)

override fun getDefault(shape: Shape?) { }

override fun structureShape(shape: StructureShape) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ private fun getDefaultRuntimeVersion(): String {
const val RUNTIME_GROUP: String = "aws.smithy.kotlin"
val RUNTIME_VERSION: String = System.getProperty("smithy.kotlin.codegen.clientRuntimeVersion", getDefaultRuntimeVersion())
val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.1.0")
val SHADOW_JAR_VERSION: String = System.getProperty("com.github.johnrengelman.shadowVersion", "8.1.1")
Comment on lines 40 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We need to stop embedding hardcoded versions into our codegen files and should be auto-discovering them instead, similar to how we do with clientRuntimeVersion above. I don't expect you to implement that, though. Please leave a // FIXME on these two lines mentioning that we should detect versions.


enum class SourceSet {
CommonMain,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ fun writeGradleBuild(
settings: KotlinSettings,
manifest: FileManifest,
dependencies: List<KotlinDependency>,
enableApplication: Boolean = false,
) {
val writer = GradleWriter()

Expand Down Expand Up @@ -53,6 +54,12 @@ fun writeGradleBuild(
}
},
)
if (enableApplication) {
indent()
write("application")
write("id(\"com.github.johnrengelman.shadow\") version #S", SHADOW_JAR_VERSION)
dedent()
}
Comment on lines +57 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: Prefer Smithy placeholders or triple-quotes to avoid escaping strings which may contain quotes.

// either
write("id(#S) version #S", "com.github.johnrengelman.shadow", SHADOW_JAR_VERSION)

// or
write("""id("com.github.johnrengelman.shadow") version #S""", SHADOW_JAR_VERSION)

Applies in other spots as well.

}

when {
Expand All @@ -74,6 +81,13 @@ fun writeGradleBuild(
)
}

if (enableApplication) {
writer.write("")
writer.openBlock("application {")
writer.write("mainClass.set(\"${settings.pkg.name}.pt.RunnerKt\")")
writer.closeBlock("}")
}
Comment on lines +84 to +89
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why do the generated projects need the application plugin if protocol-tests-runner already has it statically configured?


val contents = writer.toString()
manifest.writeFile("build.gradle.kts", contents)
if (settings.build.generateFullProject) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package software.amazon.smithy.kotlin.codegen.rendering.protocol

import software.amazon.smithy.codegen.core.SymbolProvider
import software.amazon.smithy.kotlin.codegen.core.KotlinDependency
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.OperationShape
Expand Down Expand Up @@ -40,9 +39,7 @@ protected constructor(
/**
* Render a test class and unit tests for the specified [testCases]
*/
fun renderTestClass(testClassName: String) {
writer.addImport(KotlinDependency.KOTLIN_TEST.namespace, "Test")

open fun renderTestClass(testClassName: String) {
writer.write("")
.openBlock("class $testClassName {")
.call {
Expand All @@ -58,7 +55,7 @@ protected constructor(
/**
* Write a single unit test function using the given [writer]
*/
private fun renderTestFunction(test: T) {
protected open fun renderTestFunction(test: T) {
test.documentation.ifPresent {
writer.dokka(it)
}
Expand Down
103 changes: 103 additions & 0 deletions codegen/smithy-kotlin-protocol-tests-codegen/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import aws.sdk.kotlin.gradle.dsl.configurePublishing
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
alias(libs.plugins.kotlin.jvm)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Does this need to be JVM only? Or can these tests be run in Kotlin/Native as well?

jacoco
`maven-publish`
}

val codegenVersion: String by project
description = "Smithy Kotlin Codegen for Protocol tests"
group = "software.amazon.smithy.kotlin"
version = codegenVersion

dependencies {
api(project(":codegen:smithy-kotlin-codegen"))

api(libs.smithy.aws.traits)
api(libs.smithy.protocol.test.traits)
api(libs.smithy.protocol.traits)

testImplementation(libs.junit.jupiter)
testImplementation(libs.junit.jupiter.params)
testImplementation(libs.kotest.assertions.core.jvm)
testImplementation(libs.kotlin.test.junit5)
testImplementation(project(":codegen:smithy-kotlin-codegen-testutils"))

testImplementation(libs.slf4j.api)
testImplementation(libs.slf4j.simple)
testImplementation(libs.kotlinx.serialization.json)
}

tasks.withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

tasks.withType<JavaCompile> {
sourceCompatibility = JavaVersion.VERSION_17.toString()
targetCompatibility = JavaVersion.VERSION_17.toString()
}

// Reusable license copySpec
val licenseSpec = copySpec {
from("${project.rootDir}/LICENSE")
from("${project.rootDir}/NOTICE")
}

// Configure jars to include license related info
tasks.jar {
metaInf.with(licenseSpec)
inputs.property("moduleName", project.name)
manifest {
attributes["Automatic-Module-Name"] = project.name
}
}

tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
showStandardStreams = true
showStackTraces = true
showExceptions = true
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}

// Configure jacoco (code coverage) to generate an HTML report
tasks.jacocoTestReport {
reports {
xml.required.set(false)
csv.required.set(false)
html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco"))
}
}

// Always run the jacoco test report after testing.
tasks["test"].finalizedBy(tasks["jacocoTestReport"])

val sourcesJar by tasks.creating(Jar::class) {
group = "publishing"
description = "Assembles Kotlin sources jar"
archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource)
}

publishing {
publications {
create<MavenPublication>("codegen") {
from(components["java"])
artifact(sourcesJar)
}
}
}

configurePublishing("smithy-kotlin", "smithy-lang")
Comment on lines +94 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why do we need to publish this project? Will we need to fetch/run it from Maven Central in some cases?

Applies in other places too.

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.kotlin.codegen.pt

Comment on lines +5 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: We should use a more descriptive namespace than pt. I suggest tests.protocol.

import software.amazon.smithy.build.PluginContext
import software.amazon.smithy.build.SmithyBuildPlugin
import software.amazon.smithy.kotlin.codegen.CodegenVisitor
import software.amazon.smithy.kotlin.codegen.core.GradleConfiguration
import software.amazon.smithy.kotlin.codegen.core.KOTLIN_COMPILER_VERSION
import software.amazon.smithy.kotlin.codegen.core.KotlinDependency
import software.amazon.smithy.kotlin.codegen.rendering.protocol.TestMemberDelta
import software.amazon.smithy.kotlin.codegen.rendering.protocol.pt.ProtocolTestGenerator
import software.amazon.smithy.kotlin.codegen.rendering.writeGradleBuild

// We redefine the kotlin-test and smithy-tests dependencies since for this use case
// we need them in the implementation scope instead of just in the test scope.
val KOTLIN_TEST_RT = KotlinDependency(
GradleConfiguration.Implementation,
"kotlin.test",
"org.jetbrains.kotlin",
"kotlin-test",
KOTLIN_COMPILER_VERSION,
)
val SMITHY_TEST_RT = KotlinDependency(
GradleConfiguration.Implementation,
KotlinDependency.SMITHY_TEST.namespace,
KotlinDependency.SMITHY_TEST.group,
KotlinDependency.SMITHY_TEST.artifact,
KotlinDependency.SMITHY_TEST.version,
)
Comment on lines +17 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could be private.

Comment on lines +17 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: What does "RT" mean in these names?

Comment on lines +17 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Prefer the generated copy method that every data class has:

val KOTLIN_TEST_RT = KOTLIN_TEST.copy(config = GradleConfiguration.Implementation)
val SMITHY_TEST_RT = SMITHY_TEST.copy(config = GradleConfiguration.Implementation)

This saves us from duplicating the other values in code and potentially diverging.


/**
* Plugin to trigger Kotlin protocol tests code generation. This plugin also generates the client and the
* request/response shapes.
*/
public class KotlinProtocolTestCodegenPlugin : SmithyBuildPlugin {

override fun getName(): String = "kotlin-protocol-tests-codegen"

override fun execute(context: PluginContext?) {
// Run the regular codegen
var codegen = CodegenVisitor(context ?: error("context was null"))
codegen.generateShapes()

// Generate the protocol tests
val ctx = codegen.generationContext()
val requestTestBuilder = ProtocolTestRequestGenerator.Builder()
val responseTestBuilder = ProtocolTestResponseGenerator.Builder()
val errorTestBuilder = ProtocolTestErrorGenerator.Builder()
val ignoredTests = TestMemberDelta(
setOf(),
)
ProtocolTestGenerator(
ctx,
requestTestBuilder,
responseTestBuilder,
errorTestBuilder,
ignoredTests,
).generateProtocolTests()
Comment on lines +47 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Doesn't generateShapes already generate the protocol tests? Why are we doing it twice?


val writers = ctx.delegator
writers.finalize()
val settings = ctx.settings

if (settings.build.generateDefaultBuildFiles) {
val dependencies = writers.dependencies
.mapNotNull { it.properties["dependency"] as? KotlinDependency }
.distinct()
val newDependencies = ArrayList<KotlinDependency>()
newDependencies.addAll(dependencies)
newDependencies.add(KOTLIN_TEST_RT)
newDependencies.add(SMITHY_TEST_RT)
writeGradleBuild(settings, writers.fileManifest, newDependencies, enableApplication = true)
}
writers.flushWriters()
Comment on lines +47 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: All of this seems like the responsibility of the CodegenVisitor and not the plugin directly. I think the architecture of this split may not be grouping concerns properly.

I suggest making CodegenVisitor more flexible and passing arguments to its constructor for the normal and special cases here. Alternatively, we could make a new visitor class for this runner use case, which inherits from the existing CodegenVisitor or from a refactored abstract parent class.

}
}
Loading
Loading