Skip to content

Commit 4cf9377

Browse files
committed
grpc: Move CoreClientTest to commonTest and provide service server
Signed-off-by: Johannes Zottele <[email protected]>
1 parent 2084752 commit 4cf9377

File tree

8 files changed

+108
-104
lines changed

8 files changed

+108
-104
lines changed

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public expect class Status {
3232

3333
public expect fun Status(code: StatusCode, description: String? = null, cause: Throwable? = null): Status
3434

35-
public expect val Status.code: StatusCode
35+
public expect val Status.statusCode: StatusCode
3636

3737
public enum class StatusCode(public val value: Int) {
3838
OK(0),

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/suspendClientCalls.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ private fun <Response> channelResponseListener(
201201
},
202202
onClose = { status: Status, trailers: GrpcTrailers ->
203203
val cause = when {
204-
status.code == StatusCode.OK -> null
204+
status.statusCode == StatusCode.OK -> null
205205
status.getCause() is CancellationException -> status.getCause()
206206
else -> StatusException(status, trailers)
207207
}
Lines changed: 93 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
/*
22
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
4-
@file:OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class, ExperimentalNativeApi::class)
4+
package kotlinx.rpc.grpc.test
55

6-
package kotlinx.rpc.grpc.internal
7-
8-
import HelloReply
9-
import HelloReplyInternal
10-
import HelloRequest
11-
import HelloRequestInternal
12-
import invoke
13-
import kotlinx.cinterop.ExperimentalForeignApi
146
import kotlinx.coroutines.CompletableDeferred
157
import kotlinx.coroutines.delay
168
import kotlinx.coroutines.runBlocking
9+
import kotlinx.coroutines.test.runTest
1710
import kotlinx.coroutines.withTimeout
1811
import kotlinx.rpc.grpc.*
19-
import kotlin.experimental.ExperimentalNativeApi
12+
import kotlinx.rpc.grpc.internal.*
13+
import kotlinx.rpc.registerService
2014
import kotlin.test.Test
2115
import kotlin.test.assertEquals
16+
import kotlin.test.assertFails
2217
import kotlin.test.assertFailsWith
23-
import kotlin.test.assertTrue
18+
import kotlin.time.Duration
2419

20+
private const val PORT = 50051
2521

22+
/**
23+
* Client tests that use lower level API directly to test that it behaves correctly.
24+
* Before executing the tests run [GreeterServiceImpl.runServer] on JVM.
25+
*/
2626
// TODO: Start external service server automatically (KRPC-208)
27-
class GrpcCoreTest {
27+
class GrpcCoreClientTest {
2828

29-
private fun descriptorFor(fullName: String = "helloworld.Greeter/SayHello"): MethodDescriptor<HelloRequest, HelloReply> =
29+
private fun descriptorFor(fullName: String = "kotlinx.rpc.grpc.test.GreeterService/SayHello"): MethodDescriptor<HelloRequest, HelloReply> =
3030
methodDescriptor(
3131
fullMethodName = fullName,
3232
requestCodec = HelloRequestInternal.CODEC,
@@ -38,10 +38,10 @@ class GrpcCoreTest {
3838
sampledToLocalTracing = true,
3939
)
4040

41-
private fun ManagedChannel.newHelloCall(fullName: String = "helloworld.Greeter/SayHello"): ClientCall<HelloRequest, HelloReply> =
42-
platformApi.newCall(descriptorFor(fullName), GrpcCallOptions())
41+
private fun ManagedChannel.newHelloCall(fullName: String = "kotlinx.rpc.grpc.test.GreeterService/SayHello"): ClientCall<HelloRequest, HelloReply> =
42+
platformApi.newCall(descriptorFor(fullName), GrpcDefaultCallOptions)
4343

44-
private fun createChannel(): ManagedChannel = ManagedChannelBuilder("localhost:50051")
44+
private fun createChannel(): ManagedChannel = ManagedChannelBuilder("localhost:$PORT")
4545
.usePlaintext()
4646
.buildChannel()
4747

@@ -64,15 +64,10 @@ class GrpcCoreTest {
6464

6565
val statusDeferred = CompletableDeferred<Status>()
6666
val replyDeferred = CompletableDeferred<HelloReply>()
67-
val listener = object : ClientCall.Listener<HelloReply>() {
68-
override fun onMessage(message: HelloReply) {
69-
replyDeferred.complete(message)
70-
}
71-
72-
override fun onClose(status: Status, trailers: GrpcTrailers) {
73-
statusDeferred.complete(status)
74-
}
75-
}
67+
val listener = createClientCallListener<HelloReply>(
68+
onMessage = { replyDeferred.complete(it) },
69+
onClose = { status, _ -> statusDeferred.complete(status) }
70+
)
7671

7772
call.start(listener, GrpcTrailers())
7873
call.sendMessage(req)
@@ -82,41 +77,22 @@ class GrpcCoreTest {
8277
runBlocking {
8378
withTimeout(10000) {
8479
val status = statusDeferred.await()
85-
val reply = replyDeferred.await()
8680
assertEquals(StatusCode.OK, status.statusCode)
81+
val reply = replyDeferred.await()
8782
assertEquals("Hello world", reply.message)
8883
}
8984
}
9085
shutdownAndWait(channel)
9186
}
9287

93-
@Test
94-
fun sendMessage_beforeStart_throws() {
95-
val channel = createChannel()
96-
val call = channel.newHelloCall()
97-
val req = helloReq()
98-
assertFailsWith<IllegalStateException> { call.sendMessage(req) }
99-
shutdownAndWait(channel)
100-
}
101-
102-
@Test
103-
fun request_beforeStart_throws() {
104-
val channel = createChannel()
105-
val call = channel.newHelloCall()
106-
assertFailsWith<IllegalStateException> { call.request(1) }
107-
shutdownAndWait(channel)
108-
}
109-
11088
@Test
11189
fun start_twice_throws() {
11290
val channel = createChannel()
11391
val call = channel.newHelloCall()
11492
val statusDeferred = CompletableDeferred<Status>()
115-
val listener = object : ClientCall.Listener<HelloReply>() {
116-
override fun onClose(status: Status, trailers: GrpcTrailers) {
117-
statusDeferred.complete(status)
118-
}
119-
}
93+
val listener = createClientCallListener<HelloReply>(
94+
onClose = { status, _ -> statusDeferred.complete(status) }
95+
)
12096
call.start(listener, GrpcTrailers())
12197
assertFailsWith<IllegalStateException> { call.start(listener, GrpcTrailers()) }
12298
// cancel to finish the call quickly
@@ -131,11 +107,9 @@ class GrpcCoreTest {
131107
val call = channel.newHelloCall()
132108
val req = helloReq()
133109
val statusDeferred = CompletableDeferred<Status>()
134-
val listener = object : ClientCall.Listener<HelloReply>() {
135-
override fun onClose(status: Status, trailers: GrpcTrailers) {
136-
statusDeferred.complete(status)
137-
}
138-
}
110+
val listener = createClientCallListener<HelloReply>(
111+
onClose = { status, _ -> statusDeferred.complete(status) }
112+
)
139113
call.start(listener, GrpcTrailers())
140114
call.halfClose()
141115
assertFailsWith<IllegalStateException> { call.sendMessage(req) }
@@ -146,17 +120,15 @@ class GrpcCoreTest {
146120
}
147121

148122
@Test
149-
fun request_zero_throws() {
123+
fun request_negative_throws() {
150124
val channel = createChannel()
151125
val call = channel.newHelloCall()
152126
val statusDeferred = CompletableDeferred<Status>()
153-
val listener = object : ClientCall.Listener<HelloReply>() {
154-
override fun onClose(status: Status, trailers: GrpcTrailers) {
155-
statusDeferred.complete(status)
156-
}
157-
}
127+
val listener = createClientCallListener<HelloReply>(
128+
onClose = { status, _ -> statusDeferred.complete(status) }
129+
)
158130
call.start(listener, GrpcTrailers())
159-
assertFailsWith<IllegalStateException> { call.request(0) }
131+
assertFails { call.request(-1) }
160132
call.cancel("cleanup", null)
161133
runBlocking { withTimeout(5000) { statusDeferred.await() } }
162134
shutdownAndWait(channel)
@@ -167,11 +139,9 @@ class GrpcCoreTest {
167139
val channel = createChannel()
168140
val call = channel.newHelloCall()
169141
val statusDeferred = CompletableDeferred<Status>()
170-
val listener = object : ClientCall.Listener<HelloReply>() {
171-
override fun onClose(status: Status, trailers: GrpcTrailers) {
172-
statusDeferred.complete(status)
173-
}
174-
}
142+
val listener = createClientCallListener<HelloReply>(
143+
onClose = { status, _ -> statusDeferred.complete(status) }
144+
)
175145
call.start(listener, GrpcTrailers())
176146
call.cancel("user cancel", null)
177147
runBlocking {
@@ -186,13 +156,11 @@ class GrpcCoreTest {
186156
@Test
187157
fun invalid_method_returnsNonOkStatus() {
188158
val channel = createChannel()
189-
val call = channel.newHelloCall("/helloworld.Greeter/NoSuchMethod")
159+
val call = channel.newHelloCall("kotlinx.rpc.grpc.test.Greeter/NoSuchMethod")
190160
val statusDeferred = CompletableDeferred<Status>()
191-
val listener = object : ClientCall.Listener<HelloReply>() {
192-
override fun onClose(status: Status, trailers: GrpcTrailers) {
193-
statusDeferred.complete(status)
194-
}
195-
}
161+
val listener = createClientCallListener<HelloReply>(
162+
onClose = { status, _ -> statusDeferred.complete(status) }
163+
)
196164

197165
call.start(listener, GrpcTrailers())
198166
call.sendMessage(helloReq())
@@ -201,7 +169,7 @@ class GrpcCoreTest {
201169
runBlocking {
202170
withTimeout(10000) {
203171
val status = statusDeferred.await()
204-
assertTrue(status.statusCode != StatusCode.OK)
172+
assertEquals(StatusCode.UNIMPLEMENTED, status.statusCode)
205173
}
206174
}
207175
shutdownAndWait(channel)
@@ -213,11 +181,9 @@ class GrpcCoreTest {
213181
val channel = createChannel()
214182
val call = channel.newHelloCall()
215183
val statusDeferred = CompletableDeferred<Status>()
216-
val listener = object : ClientCall.Listener<HelloReply>() {
217-
override fun onClose(status: Status, trailers: GrpcTrailers) {
218-
statusDeferred.complete(status)
219-
}
220-
}
184+
val listener = createClientCallListener<HelloReply>(
185+
onClose = { status, _ -> statusDeferred.complete(status) }
186+
)
221187
assertFailsWith<IllegalStateException> {
222188
try {
223189
call.start(listener, GrpcTrailers())
@@ -234,13 +200,12 @@ class GrpcCoreTest {
234200
val channel = createChannel()
235201
val call = channel.newHelloCall()
236202
val statusDeferred = CompletableDeferred<Status>()
237-
val listener = object : ClientCall.Listener<HelloReply>() {
238-
override fun onClose(status: Status, trailers: GrpcTrailers) {
239-
statusDeferred.complete(status)
240-
}
241-
}
203+
val listener = createClientCallListener<HelloReply>(
204+
onClose = { status, _ -> statusDeferred.complete(status) }
205+
)
242206

243207
channel.shutdown()
208+
runBlocking { delay(100) }
244209
call.start(listener, GrpcTrailers())
245210
call.sendMessage(helloReq())
246211
call.halfClose()
@@ -259,11 +224,9 @@ class GrpcCoreTest {
259224
val channel = createChannel()
260225
val call = channel.newHelloCall()
261226
val statusDeferred = CompletableDeferred<Status>()
262-
val listener = object : ClientCall.Listener<HelloReply>() {
263-
override fun onClose(status: Status, trailers: GrpcTrailers) {
264-
statusDeferred.complete(status)
265-
}
266-
}
227+
val listener = createClientCallListener<HelloReply>(
228+
onClose = { status, _ -> statusDeferred.complete(status) }
229+
)
267230

268231
call.start(listener, GrpcTrailers())
269232
// set timeout on the server to 1000 ms, to simulate a long-running call
@@ -276,19 +239,53 @@ class GrpcCoreTest {
276239
channel.shutdownNow()
277240
withTimeout(10000) {
278241
val status = statusDeferred.await()
279-
assertEquals(StatusCode.CANCELLED, status.statusCode)
242+
assertEquals(StatusCode.UNAVAILABLE, status.statusCode)
280243
}
281244
}
282245
}
246+
}
247+
248+
class GreeterServiceImpl : GreeterService {
249+
250+
override suspend fun SayHello(message: HelloRequest): HelloReply {
251+
delay(message.timeout?.toLong() ?: 0)
252+
return HelloReply {
253+
this.message = "Hello ${message.name}"
254+
}
255+
}
283256

257+
258+
/**
259+
* Run this on JVM before executing tests.
260+
*/
284261
@Test
285-
fun unaryCallTest() = runBlocking {
286-
val ch = createChannel()
287-
val desc = descriptorFor()
288-
val req = helloReq()
289-
repeat(1000) {
290-
val res = unaryRpc(ch.platformApi, desc, req)
291-
assertEquals("Hello world", res.message)
262+
fun runServer() = runTest(timeout = Duration.INFINITE) {
263+
val server = GrpcServer(
264+
port = PORT,
265+
builder = { registerService<GreeterService> { GreeterServiceImpl() } }
266+
)
267+
268+
try {
269+
server.start()
270+
println("Server started")
271+
server.awaitTermination()
272+
} finally {
273+
server.shutdown()
274+
server.awaitTermination()
292275
}
293276
}
294-
}
277+
278+
}
279+
280+
281+
private fun <T> createClientCallListener(
282+
onHeaders: (headers: GrpcTrailers) -> Unit = {},
283+
onMessage: (message: T) -> Unit = {},
284+
onClose: (status: Status, trailers: GrpcTrailers) -> Unit = { _, _ -> },
285+
onReady: () -> Unit = {},
286+
) = clientCallListener(
287+
onHeaders = onHeaders,
288+
onMessage = onMessage,
289+
onClose = onClose,
290+
onReady = onReady,
291+
)

grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/RawClientTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import kotlinx.rpc.grpc.internal.*
1616
import kotlinx.rpc.registerService
1717
import kotlin.test.Test
1818
import kotlin.test.assertEquals
19+
import kotlin.time.Duration
1920

2021
private const val PORT = 50051
2122

@@ -148,7 +149,7 @@ class EchoServiceImpl : EchoService {
148149
* Run this on JVM before executing tests.
149150
*/
150151
@Test
151-
fun runServer() = runTest {
152+
fun runServer() = runTest(timeout = Duration.INFINITE) {
152153
val server = GrpcServer(
153154
port = PORT,
154155
builder = { registerService<EchoService> { EchoServiceImpl() } }

grpc/grpc-core/src/commonTest/proto/helloworld.proto renamed to grpc/grpc-core/src/commonTest/proto/helloworld_grpc.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
syntax = "proto3";
22

3+
package kotlinx.rpc.grpc.test;
34

45
// The request message containing the user's name.
56
message HelloRequest {
@@ -12,3 +13,9 @@ message HelloRequest {
1213
message HelloReply {
1314
string message = 1;
1415
}
16+
17+
// The greeting service definition.
18+
service GreeterService {
19+
// Sends a greeting
20+
rpc SayHello(HelloRequest) returns (HelloReply) {}
21+
}

grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Status.jvm.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal fun StatusCode.toJvm(): io.grpc.Status.Code {
3030

3131
public actual typealias Status = io.grpc.Status
3232

33-
public actual val Status.code: StatusCode
33+
public actual val Status.statusCode: StatusCode
3434
get() = when (this.code) {
3535
io.grpc.Status.Code.OK -> StatusCode.OK
3636
io.grpc.Status.Code.CANCELLED -> StatusCode.CANCELLED

grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Status.native.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ package kotlinx.rpc.grpc
77
public actual class Status internal constructor(
88
private val description: String?,
99
internal val statusCode: StatusCode,
10-
private val cause: Throwable?
10+
private val cause: Throwable?,
1111
) {
1212
public actual fun getDescription(): String? = description
1313

1414
public actual fun getCause(): Throwable? = cause
1515
}
1616

17-
public actual val Status.code: StatusCode
17+
public actual val Status.statusCode: StatusCode
1818
get() = this.statusCode
1919

2020
public actual fun Status(

0 commit comments

Comments
 (0)