From b9aa8f523ebaa687a4be45358ad0b62f3f586604 Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Thu, 9 Nov 2023 11:43:31 -0600 Subject: [PATCH 1/4] Add conformance tests for lite runtime Update the conformance/google-javalite project to run tests with the lite runtime. --- conformance/buf.gen.yaml | 4 + conformance/common/build.gradle.kts | 3 + .../conformance/BaseConformanceTest.kt | 132 ++++ .../com/connectrpc/conformance/ServerType.kt | 0 .../ConformanceTest.kt} | 121 +-- conformance/google-javalite/build.gradle.kts | 5 +- .../conformance/javalite/ConformanceTest.kt | 704 ++++++++++++++++++ 7 files changed, 856 insertions(+), 113 deletions(-) create mode 100644 conformance/common/src/main/kotlin/com/connectrpc/conformance/BaseConformanceTest.kt rename conformance/{google-java/src/test => common/src/main}/kotlin/com/connectrpc/conformance/ServerType.kt (100%) rename conformance/google-java/src/test/kotlin/com/connectrpc/conformance/{Conformance.kt => java/ConformanceTest.kt} (85%) create mode 100644 conformance/google-javalite/src/test/kotlin/com/connectrpc/conformance/javalite/ConformanceTest.kt diff --git a/conformance/buf.gen.yaml b/conformance/buf.gen.yaml index 4f31b859..66c896cf 100644 --- a/conformance/buf.gen.yaml +++ b/conformance/buf.gen.yaml @@ -20,6 +20,10 @@ plugins: - plugin: connect-kotlin out: google-javalite/build/generated/sources/bufgen path: ./protoc-gen-connect-kotlin/build/install/protoc-gen-connect-kotlin/bin/protoc-gen-connect-kotlin + opt: + - generateCallbackMethods=true + - generateCoroutineMethods=true + - generateBlockingUnaryMethods=true - plugin: java out: google-javalite/build/generated/sources/bufgen protoc_path: .tmp/bin/protoc diff --git a/conformance/common/build.gradle.kts b/conformance/common/build.gradle.kts index 53028a9c..86cc3520 100644 --- a/conformance/common/build.gradle.kts +++ b/conformance/common/build.gradle.kts @@ -5,5 +5,8 @@ plugins { dependencies { implementation(libs.okio.core) implementation(libs.okhttp.tls) + implementation(libs.junit) implementation(libs.assertj) + implementation(libs.testcontainers) + implementation(project(":okhttp")) } diff --git a/conformance/common/src/main/kotlin/com/connectrpc/conformance/BaseConformanceTest.kt b/conformance/common/src/main/kotlin/com/connectrpc/conformance/BaseConformanceTest.kt new file mode 100644 index 00000000..e0addf00 --- /dev/null +++ b/conformance/common/src/main/kotlin/com/connectrpc/conformance/BaseConformanceTest.kt @@ -0,0 +1,132 @@ +// Copyright 2022-2023 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.connectrpc.conformance + +import com.connectrpc.ProtocolClientConfig +import com.connectrpc.RequestCompression +import com.connectrpc.SerializationStrategy +import com.connectrpc.compression.GzipCompressionPool +import com.connectrpc.conformance.ssl.sslContext +import com.connectrpc.impl.ProtocolClient +import com.connectrpc.okhttp.ConnectOkHttpClient +import com.connectrpc.protocols.NetworkProtocol +import okhttp3.OkHttpClient +import okhttp3.Protocol +import org.junit.ClassRule +import org.junit.runners.Parameterized +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy +import java.time.Duration +import java.util.Base64 + +abstract class BaseConformanceTest( + private val protocol: NetworkProtocol, + private val serverType: ServerType, +) { + lateinit var connectClient: ProtocolClient + lateinit var shortTimeoutConnectClient: ProtocolClient + + companion object { + private const val CONFORMANCE_VERSION = "88f85130640b46c0837e0d58c0484d83a110f418" + + @JvmStatic + @Parameterized.Parameters(name = "client={0},server={1}") + fun data(): Iterable> { + return arrayListOf( + arrayOf(NetworkProtocol.CONNECT, ServerType.CONNECT_GO), + arrayOf(NetworkProtocol.GRPC, ServerType.CONNECT_GO), + arrayOf(NetworkProtocol.GRPC_WEB, ServerType.CONNECT_GO), + arrayOf(NetworkProtocol.GRPC, ServerType.GRPC_GO), + ) + } + + @JvmField + @ClassRule + val CONFORMANCE_CONTAINER_CONNECT = GenericContainer("connectrpc/conformance:$CONFORMANCE_VERSION") + .withExposedPorts(8080, 8081) + .withCommand( + "/usr/local/bin/serverconnect", + "--h1port", + "8080", + "--h2port", + "8081", + "--cert", + "cert/localhost.crt", + "--key", + "cert/localhost.key", + ) + .waitingFor(HostPortWaitStrategy().forPorts(8081)) + + @JvmField + @ClassRule + val CONFORMANCE_CONTAINER_GRPC = GenericContainer("connectrpc/conformance:$CONFORMANCE_VERSION") + .withExposedPorts(8081) + .withCommand( + "/usr/local/bin/servergrpc", + "--port", + "8081", + "--cert", + "cert/localhost.crt", + "--key", + "cert/localhost.key", + ) + .waitingFor(HostPortWaitStrategy().forPorts(8081)) + } + + fun init(serializationStrategy: SerializationStrategy) { + val serverPort = if (serverType == ServerType.CONNECT_GO) CONFORMANCE_CONTAINER_CONNECT.getMappedPort(8081) else CONFORMANCE_CONTAINER_GRPC.getMappedPort(8081) + val host = "https://localhost:$serverPort" + val (sslSocketFactory, trustManager) = sslContext() + val client = OkHttpClient.Builder() + .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) + .connectTimeout(Duration.ofMinutes(1)) + .readTimeout(Duration.ofMinutes(1)) + .writeTimeout(Duration.ofMinutes(1)) + .callTimeout(Duration.ofMinutes(1)) + .sslSocketFactory(sslSocketFactory, trustManager) + .build() + shortTimeoutConnectClient = ProtocolClient( + httpClient = ConnectOkHttpClient( + client.newBuilder() + .connectTimeout(Duration.ofMillis(1)) + .readTimeout(Duration.ofMillis(1)) + .writeTimeout(Duration.ofMillis(1)) + .callTimeout(Duration.ofMillis(1)) + .build(), + ), + ProtocolClientConfig( + host = host, + serializationStrategy = serializationStrategy, + networkProtocol = protocol, + requestCompression = RequestCompression(10, GzipCompressionPool), + compressionPools = listOf(GzipCompressionPool), + ), + ) + connectClient = ProtocolClient( + httpClient = ConnectOkHttpClient(client), + ProtocolClientConfig( + host = host, + serializationStrategy = serializationStrategy, + networkProtocol = protocol, + requestCompression = RequestCompression(10, GzipCompressionPool), + compressionPools = listOf(GzipCompressionPool), + ), + ) + } + + fun b64Encode(trailingValue: ByteArray): String { + return String(Base64.getEncoder().encode(trailingValue)) + } +} diff --git a/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/ServerType.kt b/conformance/common/src/main/kotlin/com/connectrpc/conformance/ServerType.kt similarity index 100% rename from conformance/google-java/src/test/kotlin/com/connectrpc/conformance/ServerType.kt rename to conformance/common/src/main/kotlin/com/connectrpc/conformance/ServerType.kt diff --git a/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt b/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/java/ConformanceTest.kt similarity index 85% rename from conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt rename to conformance/google-java/src/test/kotlin/com/connectrpc/conformance/java/ConformanceTest.kt index 9bbd9f13..7bc75979 100644 --- a/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt +++ b/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/java/ConformanceTest.kt @@ -12,14 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.connectrpc.conformance +package com.connectrpc.conformance.java import com.connectrpc.Code import com.connectrpc.ConnectException -import com.connectrpc.ProtocolClientConfig -import com.connectrpc.RequestCompression -import com.connectrpc.compression.GzipCompressionPool -import com.connectrpc.conformance.ssl.sslContext +import com.connectrpc.conformance.BaseConformanceTest +import com.connectrpc.conformance.ServerType import com.connectrpc.conformance.v1.ErrorDetail import com.connectrpc.conformance.v1.PayloadType import com.connectrpc.conformance.v1.StreamingOutputCallResponse @@ -34,8 +32,6 @@ import com.connectrpc.conformance.v1.streamingInputCallRequest import com.connectrpc.conformance.v1.streamingOutputCallRequest import com.connectrpc.extensions.GoogleJavaProtobufStrategy import com.connectrpc.getOrThrow -import com.connectrpc.impl.ProtocolClient -import com.connectrpc.okhttp.ConnectOkHttpClient import com.connectrpc.protocols.NetworkProtocol import com.google.protobuf.ByteString import com.google.protobuf.empty @@ -44,120 +40,27 @@ import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Protocol import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.fail import org.junit.Before -import org.junit.ClassRule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import org.junit.runners.Parameterized.Parameters -import org.testcontainers.containers.GenericContainer -import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy -import java.time.Duration -import java.util.Base64 import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @RunWith(Parameterized::class) -class Conformance( - private val protocol: NetworkProtocol, - private val serverType: ServerType, -) { - private lateinit var connectClient: ProtocolClient - private lateinit var shortTimeoutConnectClient: ProtocolClient +class ConformanceTest( + protocol: NetworkProtocol, + serverType: ServerType, +): BaseConformanceTest(protocol, serverType) { + private lateinit var unimplementedServiceClient: UnimplementedServiceClient private lateinit var testServiceConnectClient: TestServiceClient - companion object { - private const val CONFORMANCE_VERSION = "88f85130640b46c0837e0d58c0484d83a110f418" - - @JvmStatic - @Parameters(name = "client={0},server={1}") - fun data(): Iterable> { - return arrayListOf( - arrayOf(NetworkProtocol.CONNECT, ServerType.CONNECT_GO), - arrayOf(NetworkProtocol.GRPC, ServerType.CONNECT_GO), - arrayOf(NetworkProtocol.GRPC_WEB, ServerType.CONNECT_GO), - arrayOf(NetworkProtocol.GRPC, ServerType.GRPC_GO), - ) - } - - @JvmField - @ClassRule - val CONFORMANCE_CONTAINER_CONNECT = GenericContainer("connectrpc/conformance:$CONFORMANCE_VERSION") - .withExposedPorts(8080, 8081) - .withCommand( - "/usr/local/bin/serverconnect", - "--h1port", - "8080", - "--h2port", - "8081", - "--cert", - "cert/localhost.crt", - "--key", - "cert/localhost.key", - ) - .waitingFor(HostPortWaitStrategy().forPorts(8081)) - - @JvmField - @ClassRule - val CONFORMANCE_CONTAINER_GRPC = GenericContainer("connectrpc/conformance:$CONFORMANCE_VERSION") - .withExposedPorts(8081) - .withCommand( - "/usr/local/bin/servergrpc", - "--port", - "8081", - "--cert", - "cert/localhost.crt", - "--key", - "cert/localhost.key", - ) - .waitingFor(HostPortWaitStrategy().forPorts(8081)) - } - @Before fun before() { - val serverPort = if (serverType == ServerType.CONNECT_GO) CONFORMANCE_CONTAINER_CONNECT.getMappedPort(8081) else CONFORMANCE_CONTAINER_GRPC.getMappedPort(8081) - val host = "https://localhost:$serverPort" - val (sslSocketFactory, trustManager) = sslContext() - val client = OkHttpClient.Builder() - .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) - .connectTimeout(Duration.ofMinutes(1)) - .readTimeout(Duration.ofMinutes(1)) - .writeTimeout(Duration.ofMinutes(1)) - .callTimeout(Duration.ofMinutes(1)) - .sslSocketFactory(sslSocketFactory, trustManager) - .build() - shortTimeoutConnectClient = ProtocolClient( - httpClient = ConnectOkHttpClient( - client.newBuilder() - .connectTimeout(Duration.ofMillis(1)) - .readTimeout(Duration.ofMillis(1)) - .writeTimeout(Duration.ofMillis(1)) - .callTimeout(Duration.ofMillis(1)) - .build(), - ), - ProtocolClientConfig( - host = host, - serializationStrategy = GoogleJavaProtobufStrategy(), - networkProtocol = protocol, - requestCompression = RequestCompression(10, GzipCompressionPool), - compressionPools = listOf(GzipCompressionPool), - ), - ) - connectClient = ProtocolClient( - httpClient = ConnectOkHttpClient(client), - ProtocolClientConfig( - host = host, - serializationStrategy = GoogleJavaProtobufStrategy(), - networkProtocol = protocol, - requestCompression = RequestCompression(10, GzipCompressionPool), - compressionPools = listOf(GzipCompressionPool), - ), - ) + init(GoogleJavaProtobufStrategy()) testServiceConnectClient = TestServiceClient(connectClient) unimplementedServiceClient = UnimplementedServiceClient(connectClient) } @@ -798,8 +701,4 @@ class Conformance( assertThat(countDownLatch.count).isZero() } } - - private fun b64Encode(trailingValue: ByteArray): String { - return String(Base64.getEncoder().encode(trailingValue)) - } -} +} \ No newline at end of file diff --git a/conformance/google-javalite/build.gradle.kts b/conformance/google-javalite/build.gradle.kts index 637acf9c..1d8100cd 100644 --- a/conformance/google-javalite/build.gradle.kts +++ b/conformance/google-javalite/build.gradle.kts @@ -22,9 +22,9 @@ sourceSets { dependencies { implementation(libs.kotlin.coroutines.core) - implementation(libs.protobuf.kotlin) + implementation(libs.protobuf.kotlinlite) implementation(project(":conformance:common")) - implementation(project(":extensions:google-java")) + implementation(project(":extensions:google-javalite")) implementation(project(":okhttp")) testImplementation(libs.okhttp.core) @@ -33,4 +33,5 @@ dependencies { testImplementation(libs.mockito) testImplementation(libs.kotlin.coroutines.core) testImplementation(libs.testcontainers) + testImplementation(libs.slf4j.simple) } diff --git a/conformance/google-javalite/src/test/kotlin/com/connectrpc/conformance/javalite/ConformanceTest.kt b/conformance/google-javalite/src/test/kotlin/com/connectrpc/conformance/javalite/ConformanceTest.kt new file mode 100644 index 00000000..ee9d449f --- /dev/null +++ b/conformance/google-javalite/src/test/kotlin/com/connectrpc/conformance/javalite/ConformanceTest.kt @@ -0,0 +1,704 @@ +// Copyright 2022-2023 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.connectrpc.conformance.javalite + +import com.connectrpc.Code +import com.connectrpc.ConnectException +import com.connectrpc.conformance.BaseConformanceTest +import com.connectrpc.conformance.ServerType +import com.connectrpc.conformance.v1.ErrorDetail +import com.connectrpc.conformance.v1.PayloadType +import com.connectrpc.conformance.v1.StreamingOutputCallResponse +import com.connectrpc.conformance.v1.TestServiceClient +import com.connectrpc.conformance.v1.UnimplementedServiceClient +import com.connectrpc.conformance.v1.echoStatus +import com.connectrpc.conformance.v1.errorDetail +import com.connectrpc.conformance.v1.payload +import com.connectrpc.conformance.v1.responseParameters +import com.connectrpc.conformance.v1.simpleRequest +import com.connectrpc.conformance.v1.streamingInputCallRequest +import com.connectrpc.conformance.v1.streamingOutputCallRequest +import com.connectrpc.extensions.GoogleJavaLiteProtobufStrategy +import com.connectrpc.getOrThrow +import com.connectrpc.protocols.NetworkProtocol +import com.google.protobuf.ByteString +import com.google.protobuf.empty +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@RunWith(Parameterized::class) +class ConformanceTest( + protocol: NetworkProtocol, + serverType: ServerType, +): BaseConformanceTest(protocol, serverType) { + + private lateinit var unimplementedServiceClient: UnimplementedServiceClient + private lateinit var testServiceConnectClient: TestServiceClient + + @Before + fun before() { + init(GoogleJavaLiteProtobufStrategy()) + testServiceConnectClient = TestServiceClient(connectClient) + unimplementedServiceClient = UnimplementedServiceClient(connectClient) + } + + @Test + fun serverStreaming(): Unit = runBlocking { + val sizes = listOf(512_000, 16, 2_028, 65_536) + val stream = testServiceConnectClient.streamingOutputCall() + val params = sizes.map { responseParameters { size = it } }.toList() + stream.sendAndClose( + streamingOutputCallRequest { + responseType = PayloadType.COMPRESSABLE + responseParameters += params + }, + ).getOrThrow() + val responses = mutableListOf() + for (response in stream.responseChannel()) { + responses.add(response) + } + assertThat(responses.map { it.payload.type }.toSet()).isEqualTo(setOf(PayloadType.COMPRESSABLE)) + assertThat(responses.map { it.payload.body.size() }).isEqualTo(sizes) + } + + @Test + fun pingPong(): Unit = runBlocking { + val stream = testServiceConnectClient.fullDuplexCall() + val responseChannel = stream.responseChannel() + listOf(512_000, 16, 2_028, 65_536).forEach { + val param = responseParameters { size = it } + stream.send( + streamingOutputCallRequest { + responseType = PayloadType.COMPRESSABLE + responseParameters += param + }, + ).getOrThrow() + val response = responseChannel.receive() + val payload = response.payload + assertThat(payload.type).isEqualTo(PayloadType.COMPRESSABLE) + assertThat(payload.body).hasSize(it) + } + stream.sendClose() + // We've already read all the messages + assertThat(responseChannel.receiveCatching().isClosed).isTrue() + } + + @Test + fun failServerStreaming(): Unit = runBlocking { + val expectedErrorDetail = errorDetail { + reason = "soirée 🎉" + domain = "connect-conformance" + } + val stream = testServiceConnectClient.failStreamingOutputCall() + val sizes = listOf( + 31415, + 9, + 2653, + 58979, + ) + val parameters = sizes.mapIndexed { index, value -> + responseParameters { + size = value + intervalUs = index * 10 + } + } + stream.sendAndClose( + streamingOutputCallRequest { + responseParameters.addAll(parameters) + }, + ) + val countDownLatch = CountDownLatch(1) + withContext(Dispatchers.IO) { + val job = async { + val responses = mutableListOf() + try { + for (response in stream.responseChannel()) { + responses.add(response) + } + fail("expected call to fail with ConnectException") + } catch (e: ConnectException) { + assertThat(responses.map { it.payload.body.size() }).isEqualTo(sizes) + assertThat(e.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(e.message).isEqualTo("soirée 🎉") + assertThat(e.unpackedDetails(ErrorDetail::class)).containsExactly(expectedErrorDetail) + } finally { + countDownLatch.countDown() + } + } + countDownLatch.await(5, TimeUnit.SECONDS) + job.cancel() + assertThat(countDownLatch.count).isZero() + } + } + + @Test + fun emptyUnary(): Unit = runBlocking { + val response = testServiceConnectClient.emptyCall(empty {}).getOrThrow() + assertThat(response).isEqualTo(empty {}) + } + + @Test + fun largeUnary(): Unit = runBlocking { + val size = 314159 + val message = simpleRequest { + responseType = PayloadType.COMPRESSABLE + responseSize = size + payload = payload { + body = ByteString.copyFrom(ByteArray(size)) + } + } + val response = testServiceConnectClient.unaryCall(message).getOrThrow() + assertThat(response.payload.body.toByteArray()).hasSize(size) + } + + @Test + fun customMetadata(): Unit = runBlocking { + val size = 314159 + val leadingKey = "x-grpc-test-echo-initial" + val leadingValue = "test_initial_metadata_value" + val trailingKey = "x-grpc-test-echo-trailing-bin" + val trailingValue = byteArrayOf(0xab.toByte(), 0xab.toByte(), 0xab.toByte()) + val headers = + mapOf( + leadingKey to listOf(leadingValue), + trailingKey to listOf(b64Encode(trailingValue)), + ) + val message = simpleRequest { + responseSize = size + payload = payload { body = ByteString.copyFrom(ByteArray(size)) } + } + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unaryCall(message, headers) { response -> + assertThat(response.code).isEqualTo(Code.OK) + assertThat(response.headers[leadingKey]).containsExactly(leadingValue) + assertThat(response.trailers[trailingKey]).containsExactly(b64Encode(trailingValue)) + response.failure { + fail("expected error to be null") + } + response.success { success -> + assertThat(success.message.payload!!.body!!.size()).isEqualTo(size) + countDownLatch.countDown() + } + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun statusCodeAndMessage(): Unit = runBlocking { + val message = simpleRequest { + responseStatus = echoStatus { + code = Code.UNKNOWN.value + message = "test status message" + } + } + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unaryCall(message) { response -> + assertThat(response.code).isEqualTo(Code.UNKNOWN) + response.failure { errorResponse -> + assertThat(errorResponse.cause).isNotNull() + assertThat(errorResponse.code).isEqualTo(Code.UNKNOWN) + assertThat(errorResponse.cause.message).isEqualTo("test status message") + countDownLatch.countDown() + } + response.success { + fail("unexpected success") + } + } + + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun timeoutOnSleepingServer(): Unit = runBlocking { + val countDownLatch = CountDownLatch(1) + val client = TestServiceClient(shortTimeoutConnectClient) + val request = streamingOutputCallRequest { + payload = payload { + body = ByteString.copyFrom(ByteArray(271828)) + } + responseParameters.add( + responseParameters { + size = 31415 + intervalUs = 50_000 + }, + ) + } + val stream = client.streamingOutputCall() + withContext(Dispatchers.IO) { + val job = launch { + try { + stream.responseChannel().receive() + fail("unexpected ConnectException to be thrown") + } catch (e: ConnectException) { + assertThat(e.code) + .withFailMessage { "Expected Code.DEADLINE_EXCEEDED but got ${e.code}" } + .isEqualTo(Code.DEADLINE_EXCEEDED) + } finally { + countDownLatch.countDown() + } + } + stream.sendAndClose(request) + countDownLatch.await(5, TimeUnit.SECONDS) + job.cancel() + assertThat(countDownLatch.count).isZero() + } + } + + @Test + fun specialStatus(): Unit = runBlocking { + val statusMessage = + "\\t\\ntest with whitespace\\r\\nand Unicode BMP ☺ and non-BMP \uD83D\uDE08\\t\\n" + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unaryCall( + simpleRequest { + responseStatus = echoStatus { + code = 2 + message = statusMessage + } + }, + ) { response -> + response.failure { errorResponse -> + val error = errorResponse.cause + assertThat(error.code).isEqualTo(Code.UNKNOWN) + assertThat(response.code).isEqualTo(Code.UNKNOWN) + assertThat(error.message).isEqualTo(statusMessage) + countDownLatch.countDown() + } + response.success { + fail("unexpected success") + } + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun unimplementedMethod(): Unit = runBlocking { + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unimplementedCall(empty {}) { response -> + assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED) + countDownLatch.countDown() + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun unimplementedService(): Unit = runBlocking { + val countDownLatch = CountDownLatch(1) + unimplementedServiceClient.unimplementedCall(empty {}) { response -> + assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED) + countDownLatch.countDown() + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun unimplementedServerStreamingService(): Unit = runBlocking { + val countDownLatch = CountDownLatch(1) + val stream = unimplementedServiceClient.unimplementedStreamingOutputCall() + stream.sendAndClose(empty { }) + withContext(Dispatchers.IO) { + val job = async { + try { + stream.responseChannel().receive() + fail("expected call to fail with a ConnectException") + } catch (e: ConnectException) { + assertThat(e.code).isEqualTo(Code.UNIMPLEMENTED) + } finally { + countDownLatch.countDown() + } + } + countDownLatch.await(5, TimeUnit.SECONDS) + job.cancel() + assertThat(countDownLatch.count).isZero() + } + } + + @Test + fun failUnary(): Unit = runBlocking { + val expectedErrorDetail = errorDetail { + reason = "soirée 🎉" + domain = "connect-conformance" + } + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.failUnaryCall(simpleRequest {}) { response -> + assertThat(response.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + response.failure { errorResponse -> + val error = errorResponse.cause + assertThat(error.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(error.message).isEqualTo("soirée 🎉") + val connectErrorDetails = error.unpackedDetails(ErrorDetail::class) + assertThat(connectErrorDetails).containsExactly(expectedErrorDetail) + countDownLatch.countDown() + } + response.success { + fail("unexpected success") + } + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun emptyUnaryBlocking(): Unit = runBlocking { + val response = testServiceConnectClient.emptyCallBlocking(empty {}).execute() + response.failure { + fail("expected error to be null") + } + response.success { success -> + assertThat(success.message).isEqualTo(empty {}) + } + } + + @Test + fun largeUnaryBlocking(): Unit = runBlocking { + val size = 314159 + val message = simpleRequest { + responseSize = size + payload = payload { + body = ByteString.copyFrom(ByteArray(size)) + } + } + val response = testServiceConnectClient.unaryCallBlocking(message).execute() + response.failure { + fail("expected error to be null") + } + response.success { success -> + assertThat(success.message.payload?.body?.toByteArray()?.size).isEqualTo(size) + } + } + + @Test + fun customMetadataBlocking(): Unit = runBlocking { + val size = 314159 + val leadingKey = "x-grpc-test-echo-initial" + val leadingValue = "test_initial_metadata_value" + val trailingKey = "x-grpc-test-echo-trailing-bin" + val trailingValue = byteArrayOf(0xab.toByte(), 0xab.toByte(), 0xab.toByte()) + val headers = + mapOf( + leadingKey to listOf(leadingValue), + trailingKey to listOf(b64Encode(trailingValue)), + ) + val message = simpleRequest { + responseSize = size + payload = payload { body = ByteString.copyFrom(ByteArray(size)) } + } + val response = testServiceConnectClient.unaryCallBlocking(message, headers).execute() + assertThat(response.code).isEqualTo(Code.OK) + assertThat(response.headers[leadingKey]).containsExactly(leadingValue) + assertThat(response.trailers[trailingKey]).containsExactly(b64Encode(trailingValue)) + response.failure { + fail("expected error to be null") + } + response.success { success -> + assertThat(success.message.payload!!.body!!.size()).isEqualTo(size) + } + } + + @Test + fun statusCodeAndMessageBlocking(): Unit = runBlocking { + val message = simpleRequest { + responseStatus = echoStatus { + code = Code.UNKNOWN.value + message = "test status message" + } + } + val response = testServiceConnectClient.unaryCallBlocking(message).execute() + assertThat(response.code).isEqualTo(Code.UNKNOWN) + response.failure { errorResponse -> + assertThat(errorResponse.cause).isNotNull() + assertThat(errorResponse.code).isEqualTo(Code.UNKNOWN) + assertThat(errorResponse.cause.message).isEqualTo("test status message") + } + response.success { + fail("unexpected success") + } + } + + @Test + fun specialStatusBlocking(): Unit = runBlocking { + val statusMessage = + "\\t\\ntest with whitespace\\r\\nand Unicode BMP ☺ and non-BMP \uD83D\uDE08\\t\\n" + val response = testServiceConnectClient.unaryCallBlocking( + simpleRequest { + responseStatus = echoStatus { + code = 2 + message = statusMessage + } + }, + ).execute() + response.failure { errorResponse -> + val error = errorResponse.cause + assertThat(error.code).isEqualTo(Code.UNKNOWN) + assertThat(response.code).isEqualTo(Code.UNKNOWN) + assertThat(error.message).isEqualTo(statusMessage) + } + response.success { + fail("unexpected success") + } + } + + @Test + fun unimplementedMethodBlocking(): Unit = runBlocking { + val response = testServiceConnectClient.unimplementedCallBlocking(empty {}).execute() + assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED) + } + + @Test + fun unimplementedServiceBlocking(): Unit = runBlocking { + val response = unimplementedServiceClient.unimplementedCallBlocking(empty {}).execute() + assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED) + } + + @Test + fun failUnaryBlocking(): Unit = runBlocking { + val expectedErrorDetail = errorDetail { + reason = "soirée 🎉" + domain = "connect-conformance" + } + val response = testServiceConnectClient.failUnaryCallBlocking(simpleRequest {}).execute() + assertThat(response.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + response.failure { errorResponse -> + val error = errorResponse.cause + assertThat(error.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(error.message).isEqualTo("soirée 🎉") + val connectErrorDetails = error.unpackedDetails(ErrorDetail::class) + assertThat(connectErrorDetails).containsExactly(expectedErrorDetail) + } + response.success { + fail("unexpected success") + } + } + + @Test + fun emptyUnaryCallback(): Unit = runBlocking { + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.emptyCall(empty {}) { response -> + response.failure { + fail("expected error to be null") + } + response.success { success -> + assertThat(success.message).isEqualTo(empty {}) + countDownLatch.countDown() + } + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun largeUnaryCallback(): Unit = runBlocking { + val size = 314159 + val message = simpleRequest { + responseSize = size + payload = payload { + body = ByteString.copyFrom(ByteArray(size)) + } + } + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unaryCall(message) { response -> + response.failure { + fail("expected error to be null") + } + response.success { success -> + assertThat(success.message.payload?.body?.toByteArray()?.size).isEqualTo(size) + countDownLatch.countDown() + } + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun customMetadataCallback(): Unit = runBlocking { + val size = 314159 + val leadingKey = "x-grpc-test-echo-initial" + val leadingValue = "test_initial_metadata_value" + val trailingKey = "x-grpc-test-echo-trailing-bin" + val trailingValue = byteArrayOf(0xab.toByte(), 0xab.toByte(), 0xab.toByte()) + val headers = + mapOf( + leadingKey to listOf(leadingValue), + trailingKey to listOf(b64Encode(trailingValue)), + ) + val message = simpleRequest { + responseSize = size + payload = payload { body = ByteString.copyFrom(ByteArray(size)) } + } + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unaryCall(message, headers) { response -> + assertThat(response.code).isEqualTo(Code.OK) + assertThat(response.headers[leadingKey]).containsExactly(leadingValue) + assertThat(response.trailers[trailingKey]).containsExactly(b64Encode(trailingValue)) + response.failure { + fail("expected error to be null") + } + response.success { success -> + assertThat(success.message.payload!!.body!!.size()).isEqualTo(size) + countDownLatch.countDown() + } + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun statusCodeAndMessageCallback(): Unit = runBlocking { + val message = simpleRequest { + responseStatus = echoStatus { + code = Code.UNKNOWN.value + message = "test status message" + } + } + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unaryCall(message) { response -> + assertThat(response.code).isEqualTo(Code.UNKNOWN) + response.failure { errorResponse -> + assertThat(errorResponse.cause).isNotNull() + assertThat(errorResponse.code).isEqualTo(Code.UNKNOWN) + assertThat(errorResponse.cause.message).isEqualTo("test status message") + countDownLatch.countDown() + } + response.success { + fail("unexpected success") + } + } + + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun specialStatusCallback(): Unit = runBlocking { + val statusMessage = + "\\t\\ntest with whitespace\\r\\nand Unicode BMP ☺ and non-BMP \uD83D\uDE08\\t\\n" + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unaryCall( + simpleRequest { + responseStatus = echoStatus { + code = 2 + message = statusMessage + } + }, + ) { response -> + response.failure { errorResponse -> + val error = errorResponse.cause + assertThat(error.code).isEqualTo(Code.UNKNOWN) + assertThat(response.code).isEqualTo(Code.UNKNOWN) + assertThat(error.message).isEqualTo(statusMessage) + countDownLatch.countDown() + } + response.success { + fail("unexpected success") + } + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun unimplementedMethodCallback(): Unit = runBlocking { + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.unimplementedCall(empty {}) { response -> + assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED) + countDownLatch.countDown() + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun unimplementedServiceCallback(): Unit = runBlocking { + val countDownLatch = CountDownLatch(1) + unimplementedServiceClient.unimplementedCall(empty {}) { response -> + assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED) + countDownLatch.countDown() + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun failUnaryCallback(): Unit = runBlocking { + val expectedErrorDetail = errorDetail { + reason = "soirée 🎉" + domain = "connect-conformance" + } + val countDownLatch = CountDownLatch(1) + testServiceConnectClient.failUnaryCall(simpleRequest {}) { response -> + assertThat(response.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + response.failure { errorResponse -> + val error = errorResponse.cause + assertThat(error.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(error.message).isEqualTo("soirée 🎉") + val connectErrorDetails = error.unpackedDetails(ErrorDetail::class) + assertThat(connectErrorDetails).containsExactly(expectedErrorDetail) + countDownLatch.countDown() + } + response.success { + fail("unexpected success") + } + } + countDownLatch.await(500, TimeUnit.MILLISECONDS) + assertThat(countDownLatch.count).isZero() + } + + @Test + fun clientStreaming(): Unit = runBlocking { + val stream = testServiceConnectClient.streamingInputCall(emptyMap()) + var sum = 0 + listOf(256000, 8, 1024, 32768).forEach { size -> + stream.send( + streamingInputCallRequest { + payload = payload { + type = PayloadType.COMPRESSABLE + body = ByteString.copyFrom(ByteArray(size)) + } + }, + ).getOrThrow() + sum += size + } + val countDownLatch = CountDownLatch(1) + withContext(Dispatchers.IO) { + val job = async { + try { + val response = stream.receiveAndClose() + assertThat(response.aggregatedPayloadSize).isEqualTo(sum) + } finally { + countDownLatch.countDown() + } + } + countDownLatch.await(5, TimeUnit.SECONDS) + job.cancel() + assertThat(countDownLatch.count).isZero() + } + } +} \ No newline at end of file From 3558b3cc2d98b3626f94eac331169401ab38799f Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Thu, 9 Nov 2023 11:47:10 -0600 Subject: [PATCH 2/4] fix lint issues --- .../kotlin/com/connectrpc/conformance/java/ConformanceTest.kt | 4 ++-- .../com/connectrpc/conformance/javalite/ConformanceTest.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/java/ConformanceTest.kt b/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/java/ConformanceTest.kt index 7bc75979..800ad92e 100644 --- a/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/java/ConformanceTest.kt +++ b/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/java/ConformanceTest.kt @@ -53,7 +53,7 @@ import java.util.concurrent.TimeUnit class ConformanceTest( protocol: NetworkProtocol, serverType: ServerType, -): BaseConformanceTest(protocol, serverType) { +) : BaseConformanceTest(protocol, serverType) { private lateinit var unimplementedServiceClient: UnimplementedServiceClient private lateinit var testServiceConnectClient: TestServiceClient @@ -701,4 +701,4 @@ class ConformanceTest( assertThat(countDownLatch.count).isZero() } } -} \ No newline at end of file +} diff --git a/conformance/google-javalite/src/test/kotlin/com/connectrpc/conformance/javalite/ConformanceTest.kt b/conformance/google-javalite/src/test/kotlin/com/connectrpc/conformance/javalite/ConformanceTest.kt index ee9d449f..3c8e552a 100644 --- a/conformance/google-javalite/src/test/kotlin/com/connectrpc/conformance/javalite/ConformanceTest.kt +++ b/conformance/google-javalite/src/test/kotlin/com/connectrpc/conformance/javalite/ConformanceTest.kt @@ -53,7 +53,7 @@ import java.util.concurrent.TimeUnit class ConformanceTest( protocol: NetworkProtocol, serverType: ServerType, -): BaseConformanceTest(protocol, serverType) { +) : BaseConformanceTest(protocol, serverType) { private lateinit var unimplementedServiceClient: UnimplementedServiceClient private lateinit var testServiceConnectClient: TestServiceClient @@ -701,4 +701,4 @@ class ConformanceTest( assertThat(countDownLatch.count).isZero() } } -} \ No newline at end of file +} From 889b6c7b96e6e61b4855bd6ec32bdb7641426fc4 Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Thu, 9 Nov 2023 12:10:48 -0600 Subject: [PATCH 3/4] run both tests in makefile and save test reports --- .github/workflows/ci.yml | 9 +++++++++ Makefile | 7 ++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 002422aa..795c8f28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,15 @@ jobs: ${{ runner.os }}-go- - name: Run conformance tests run: make conformancerun + - name: Update test reports + uses: actions/upload-artifact@v3 + if: always() + with: + name: conformance-test-reports + path: | + conformance/google-java/build/reports/tests + conformance/google-javalite/build/reports/tests + retention-days: 7 license-headers: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 19952de0..71f6732f 100644 --- a/Makefile +++ b/Makefile @@ -36,11 +36,8 @@ clean: ## Cleans the underlying build. ./gradlew $(GRADLE_ARGS) clean .PHONY: conformancerun -conformancerun: conformancerunjava ## Run the conformance tests. - -.PHONY: conformancerunjava -conformancerunjava: generate ## Run the conformance tests for protoc-gen-java integration. - ./gradlew $(GRADLE_ARGS) conformance:google-java:test +conformancerun: generate ## Run the conformance tests. + ./gradlew $(GRADLE_ARGS) conformance:google-java:test conformance:google-javalite:test ifeq ($(UNAME_OS),Darwin) PROTOC_OS := osx From fad66a3be79a400e6a402dbdc0e8e0da1f4427d0 Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Thu, 9 Nov 2023 12:13:23 -0600 Subject: [PATCH 4/4] fix typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 795c8f28..b6cb9847 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: ${{ runner.os }}-go- - name: Run conformance tests run: make conformancerun - - name: Update test reports + - name: Upload test reports uses: actions/upload-artifact@v3 if: always() with: