Skip to content

Commit 1a3d2bf

Browse files
committed
Added backwards compatibility tests
1 parent aba3699 commit 1a3d2bf

File tree

14 files changed

+1190
-0
lines changed

14 files changed

+1190
-0
lines changed

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ includePublic(":krpc:krpc-ktor:krpc-ktor-client")
5353

5454
include(":tests")
5555
include(":tests:krpc-compatibility-tests")
56+
include(":tests:krpc-protocol-compatibility-tests")
5657

5758
val kotlinMasterBuild = providers.gradleProperty("kotlinx.rpc.kotlinMasterBuild").orNull == "true"
5859

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:Suppress("PropertyName")
6+
7+
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
8+
9+
plugins {
10+
alias(libs.plugins.conventions.jvm)
11+
alias(libs.plugins.serialization)
12+
alias(libs.plugins.kotlinx.rpc)
13+
alias(libs.plugins.atomicfu)
14+
}
15+
16+
val main: SourceSet by sourceSets.getting
17+
val test: SourceSet by sourceSets.getting
18+
19+
val compatibilityTestSourcesDir: File = project.layout.buildDirectory.dir("compatibilityTestSources").get().asFile
20+
21+
fun versioned(name: String): Configuration {
22+
val configuration = configurations.create(name) {
23+
isCanBeConsumed = true
24+
isCanBeResolved = true
25+
isTransitive = true
26+
}
27+
28+
val sourceSet = sourceSets.create(name) {
29+
compileClasspath += main.output
30+
runtimeClasspath += main.output
31+
32+
compileClasspath += configuration
33+
runtimeClasspath += configuration
34+
}
35+
36+
val copySourceSetTestResources by tasks.register<Copy>("copy_${name}_ToTestResources") {
37+
dependsOn(sourceSet.output)
38+
from(sourceSet.output)
39+
into(compatibilityTestSourcesDir.resolve(name))
40+
}
41+
42+
tasks.processTestResources.configure {
43+
dependsOn(copySourceSetTestResources)
44+
}
45+
46+
return configuration
47+
}
48+
49+
val v0_9 = versioned("v0_9")
50+
val v0_8 = versioned("v0_8")
51+
52+
test.resources {
53+
srcDir(compatibilityTestSourcesDir)
54+
}
55+
56+
fun DependencyHandlerScope.versioned(configuration: Configuration, version: String) {
57+
add(configuration.name, "org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:$version")
58+
add(configuration.name, "org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:$version")
59+
add(configuration.name, "org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:$version")
60+
add(configuration.name, libs.atomicfu)
61+
}
62+
63+
dependencies {
64+
api(libs.atomicfu)
65+
implementation(libs.serialization.core)
66+
implementation(libs.coroutines.core)
67+
implementation(libs.kotlin.reflect)
68+
69+
versioned(v0_9, "0.9.1")
70+
versioned(v0_8, "0.8.1")
71+
72+
// current version is in test source set
73+
testImplementation(projects.krpc.krpcCore)
74+
testImplementation(projects.krpc.krpcServer)
75+
testImplementation(projects.krpc.krpcClient)
76+
testImplementation(projects.krpc.krpcSerialization.krpcSerializationJson)
77+
78+
testImplementation(libs.coroutines.test)
79+
testImplementation(libs.kotlin.test.junit5)
80+
81+
testImplementation(libs.slf4j.api)
82+
testImplementation(libs.logback.classic)
83+
testImplementation(libs.coroutines.debug)
84+
}
85+
86+
kotlin {
87+
explicitApi = ExplicitApiMode.Disabled
88+
}
89+
90+
tasks.test {
91+
useJUnitPlatform()
92+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.krpc.test.compat
6+
7+
import kotlinx.coroutines.CompletableDeferred
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.flow.Flow
10+
import kotlinx.coroutines.yield
11+
12+
interface CompatTransport : CoroutineScope {
13+
suspend fun send(message: String)
14+
suspend fun receive(): String
15+
}
16+
17+
class TestConfig(
18+
val perCallBufferSize: Int,
19+
)
20+
21+
interface CompatService {
22+
suspend fun unary(n: Int): Int
23+
fun serverStreaming(num: Int): Flow<Int>
24+
suspend fun clientStreaming(n: Flow<Int>): Int
25+
fun bidiStreaming(flow1: Flow<Int>, flow2: Flow<Int>): Flow<Int>
26+
27+
suspend fun requestCancellation()
28+
fun serverStreamCancellation(): Flow<Int>
29+
suspend fun clientStreamCancellation(n: Flow<Int>)
30+
31+
fun fastServerProduce(n: Int): Flow<Int>
32+
}
33+
34+
interface CompatServiceImpl {
35+
val exitMethod: Int
36+
val cancelled: Int
37+
val entered: CompletableDeferred<Unit>
38+
val fence: CompletableDeferred<Unit>
39+
40+
suspend fun awaitCounter(num: Int, counter: CompatServiceImpl.() -> Int) {
41+
while (counter() != num) {
42+
yield()
43+
}
44+
}
45+
}
46+
47+
interface Starter {
48+
suspend fun startClient(transport: CompatTransport, config: TestConfig): CompatService
49+
suspend fun stopClient()
50+
suspend fun startServer(transport: CompatTransport, config: TestConfig): CompatServiceImpl
51+
suspend fun stopServer()
52+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.krpc.test.compat
6+
7+
import kotlinx.coroutines.async
8+
import kotlinx.coroutines.cancelAndJoin
9+
import kotlinx.coroutines.flow.asFlow
10+
import kotlinx.coroutines.flow.flow
11+
import kotlinx.coroutines.flow.map
12+
import kotlinx.coroutines.flow.toList
13+
import kotlinx.coroutines.joinAll
14+
import kotlinx.coroutines.launch
15+
import org.junit.jupiter.api.TestFactory
16+
import kotlin.test.assertEquals
17+
import kotlin.time.Duration.Companion.seconds
18+
19+
class KrpcProtocolCompatibilityTests : KrpcProtocolCompatibilityTestsBase() {
20+
@TestFactory
21+
fun unaryCalls() = matrixTest { service, _ ->
22+
assertEquals(1, service.unary(1))
23+
24+
List(100) {
25+
launch {
26+
assertEquals(it + 1, service.unary(it + 1))
27+
}
28+
}.joinAll()
29+
30+
assertNoErrorsInLogs()
31+
}
32+
33+
@TestFactory
34+
fun serverStreamCalls() = matrixTest { service, _ ->
35+
assertEquals(1, service.serverStreaming(1).toList().sum())
36+
37+
List(100) {
38+
launch {
39+
assertEquals((it + 1) * (it + 2) / 2, service.serverStreaming(it + 1).toList().sum())
40+
}
41+
}.joinAll()
42+
43+
assertNoErrorsInLogs()
44+
}
45+
46+
@TestFactory
47+
fun clientStreamCalls() = matrixTest { service, _ ->
48+
assertEquals(1, service.clientStreaming((1..1).asFlow()))
49+
50+
List(100) {
51+
launch {
52+
assertEquals(
53+
(it + 1) * (it + 2) / 2,
54+
service.clientStreaming((1..it + 1).asFlow()),
55+
)
56+
}
57+
}.joinAll()
58+
59+
assertNoErrorsInLogs()
60+
}
61+
62+
@TestFactory
63+
fun bidiStreamCalls() = matrixTest { service, _ ->
64+
assertEquals(
65+
2,
66+
service.bidiStreaming((1..1).asFlow(), (1..1).asFlow()).toList().sum()
67+
)
68+
69+
List(100) {
70+
launch {
71+
assertEquals(
72+
(it + 1) * (it + 2),
73+
service.bidiStreaming((1..it + 1).asFlow(), (1..it + 1).asFlow()).toList().sum(),
74+
)
75+
}
76+
}.joinAll()
77+
78+
assertNoErrorsInLogs()
79+
}
80+
81+
@TestFactory
82+
fun requestCancellation() = matrixTest { service, impl ->
83+
val job = launch {
84+
service.requestCancellation()
85+
}
86+
87+
impl.entered.await()
88+
job.cancelAndJoin()
89+
impl.awaitCounter(1) { cancelled }
90+
assertEquals(0, impl.exitMethod)
91+
92+
val followup = launch {
93+
service.requestCancellation()
94+
}
95+
impl.fence.complete(Unit)
96+
followup.join()
97+
assertEquals(1, impl.exitMethod)
98+
99+
assertNoErrorsInLogs()
100+
assertEquals(1, impl.cancelled)
101+
}
102+
103+
@TestFactory
104+
fun serverStreamCancellation() = matrixTest { service, impl ->
105+
val job = launch {
106+
service.serverStreamCancellation().collect {}
107+
}
108+
109+
impl.entered.await()
110+
job.cancelAndJoin()
111+
impl.awaitCounter(1) { cancelled }
112+
assertEquals(0, impl.exitMethod)
113+
114+
val followup = async {
115+
service.serverStreamCancellation().toList()
116+
}
117+
impl.fence.complete(Unit)
118+
assertEquals(listOf(1, 2), followup.await())
119+
120+
assertNoErrorsInLogs()
121+
assertEquals(1, impl.cancelled)
122+
}
123+
124+
@TestFactory
125+
fun clientStreamCancellation() = matrixTest { service, impl ->
126+
val job = launch {
127+
service.clientStreamCancellation(flow {
128+
emit(1)
129+
impl.fence.await()
130+
})
131+
}
132+
133+
impl.entered.await()
134+
job.cancelAndJoin()
135+
impl.awaitCounter(1) { cancelled }
136+
137+
assertNoErrorsInLogs()
138+
}
139+
140+
@TestFactory
141+
fun fastProducer() = matrixTest(timeout = 30.seconds) { service, impl ->
142+
val async = async {
143+
service.fastServerProduce(1000).map {
144+
// long produce
145+
impl.entered.complete(Unit)
146+
impl.fence.await()
147+
it * it
148+
}.toList()
149+
}
150+
151+
impl.entered.await()
152+
repeat(10_000) {
153+
assertEquals(1, service.unary(1))
154+
assertEquals(55, service.serverStreaming(10).toList().sum())
155+
}
156+
157+
impl.fence.complete(Unit)
158+
assertEquals(List(1000) { it * it },async.await())
159+
}
160+
}

0 commit comments

Comments
 (0)