Skip to content

Commit

Permalink
KTOR-8057 Introduce base class for client engine tests (#4598)
Browse files Browse the repository at this point in the history
* KTOR-8058 Don't publish test projects
* Move ClientLoader to ktor-client-test-base
* Create ClientEngineTest
* Migrate native client engine tests to base class
  • Loading branch information
osipxd authored Jan 14, 2025
1 parent db67b9f commit c47dfda
Show file tree
Hide file tree
Showing 115 changed files with 806 additions and 677 deletions.
20 changes: 7 additions & 13 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ val configuredVersion: String by extra

apply(from = "gradle/verifier.gradle")

extra["skipPublish"] = mutableListOf(
val internalProjects = listOf(
"ktor-client-test-base",
"ktor-client-tests",
"ktor-server-test-base",
"ktor-server-test-suites",
"ktor-server-tests",
"ktor-client-content-negotiation-tests",
"ktor-test-base",
)

Expand All @@ -42,15 +46,6 @@ extra["nonDefaultProjectStructure"] = mutableListOf(
"ktor-java-modules-test",
)

val disabledExplicitApiModeProjects = listOf(
"ktor-client-tests",
"ktor-server-test-base",
"ktor-server-test-suites",
"ktor-server-tests",
"ktor-client-content-negotiation-tests",
"ktor-test-base"
)

apply(from = "gradle/compatibility.gradle")

plugins {
Expand Down Expand Up @@ -80,7 +75,7 @@ subprojects {
}

kotlin {
if (!disabledExplicitApiModeProjects.contains(project.name)) explicitApi()
if (!internalProjects.contains(project.name)) explicitApi()

configureSourceSets()
setupJvmToolchain()
Expand All @@ -91,8 +86,7 @@ subprojects {
}
}

val skipPublish: List<String> by rootProject.extra
if (!skipPublish.contains(project.name)) {
if (!internalProjects.contains(project.name)) {
configurePublication()
}

Expand Down
5 changes: 5 additions & 0 deletions gradle/compatibility.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
/*
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

apply plugin: 'binary-compatibility-validator'

apiValidation {
def excludeList = [
'ktor',
"ktor-client-test-base",
'ktor-client-tests',
'ktor-client-js',
'ktor-client-content-negotiation-tests',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
/*
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.android

import io.ktor.client.call.*
import io.ktor.client.engine.*
import io.ktor.client.request.*
import io.ktor.client.tests.utils.*
import java.net.*
import kotlin.test.*
import io.ktor.client.test.base.*
import java.net.InetSocketAddress
import java.net.Proxy
import kotlin.test.Test
import kotlin.test.assertEquals

private const val HTTP_PROXY_PORT = 8082

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.android
Expand All @@ -16,13 +16,15 @@ import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.*
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeAll
import java.io.*
import java.security.*
import javax.net.ssl.*
import kotlin.test.*
import java.io.File
import java.security.KeyStore
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import kotlin.test.Test
import kotlin.test.assertEquals

class AndroidSpecificHttpsTest : TestWithKtor() {
override val server: EmbeddedServer<*, *> = embeddedServer(
Expand Down
168 changes: 86 additions & 82 deletions ktor-client/ktor-client-cio/common/test/CIOEngineTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.cio
Expand All @@ -8,75 +8,78 @@ import io.ktor.client.*
import io.ktor.client.plugins.sse.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.client.tests.utils.*
import io.ktor.client.statement.*
import io.ktor.client.test.base.*
import io.ktor.http.*
import io.ktor.network.selector.*
import io.ktor.network.sockets.*
import io.ktor.test.dispatcher.*
import io.ktor.utils.io.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.launch
import kotlin.test.*
import kotlin.time.Duration.Companion.seconds

class CIOEngineTest {
class CIOEngineTest : ClientEngineTest<CIOEngineConfig>(CIO) {

private val selectorManager = SelectorManager()

@Test
fun testRequestTimeoutIgnoredWithWebSocket() = runTestWithRealTime {
val client = HttpClient(CIO) {
fun testRequestTimeoutIgnoredWithWebSocket() = testClient {
config {
engine {
requestTimeout = 10
}

install(WebSockets)
}

var received = false
client.ws("$TEST_WEBSOCKET_SERVER/websockets/echo") {
delay(20)
test { client ->
var received = false
client.ws("$TEST_WEBSOCKET_SERVER/websockets/echo") {
delay(20)

send(Frame.Text("Hello"))
send(Frame.Text("Hello"))

val response = incoming.receive() as Frame.Text
received = true
assertEquals("Hello", response.readText())
}
val response = incoming.receive() as Frame.Text
received = true
assertEquals("Hello", response.readText())
}

assertTrue(received)
assertTrue(received)
}
}

@Test
fun testRequestTimeoutIgnoredWithSSE() = runTestWithRealTime {
val client = HttpClient(CIO) {
fun testRequestTimeoutIgnoredWithSSE() = testClient {
config {
engine {
requestTimeout = 10
}

install(SSE)
}

var received = false
client.sse("$TEST_SERVER/sse/hello?delay=20") {
val response = incoming.single()
received = true
assertEquals("hello\r\nfrom server", response.data)
test { client ->
var received = false
client.sse("$TEST_SERVER/sse/hello?delay=20") {
val response = incoming.single()
received = true
assertEquals("hello\r\nfrom server", response.data)
}
assertTrue(received)
}
assertTrue(received)
}

@Test
fun testExpectHeader() = runTestWithRealTime {
fun testExpectHeader() = testClient {
val body = "Hello World"

withServerSocket { socket ->
val client = HttpClient(CIO)
launch {
sendExpectRequest(socket, client, body).apply {
assertEquals(HttpStatusCode.OK, status)
}
withServerSocket { client, socket ->
sendExpectRequest(socket, client, body) {
assertEquals(HttpStatusCode.OK, status)
}

socket.accept().use {
Expand All @@ -96,13 +99,10 @@ class CIOEngineTest {
}

@Test
fun testNoExpectHeaderIfNoBody() = runTestWithRealTime {
withServerSocket { socket ->
val client = HttpClient(CIO)
launch {
sendExpectRequest(socket, client).apply {
assertEquals(HttpStatusCode.OK, status)
}
fun testNoExpectHeaderIfNoBody() = testClient {
withServerSocket { client, socket ->
sendExpectRequest(socket, client) {
assertEquals(HttpStatusCode.OK, status)
}

socket.accept().use {
Expand All @@ -117,47 +117,41 @@ class CIOEngineTest {
}

@Test
fun testDontWaitForContinueResponse() = runTestWithRealTime {
withTimeout(30.seconds) {
val body = "Hello World\n"

withServerSocket { socket ->
val client = HttpClient(CIO) {
engine {
requestTimeout = 0
}
}
launch {
sendExpectRequest(socket, client, body).apply {
assertEquals(HttpStatusCode.OK, status)
}
}

socket.accept().use {
val readChannel = it.openReadChannel()
val writeChannel = it.openWriteChannel()

val headers = readAvailableLines(readChannel)
delay(2000)
val actualBody = readAvailableLine(readChannel)
assertTrue(headers.contains(EXPECT_HEADER))
assertEquals(body, actualBody)
writeOkResponse(writeChannel)
}
fun testDontWaitForContinueResponse() = testClient(timeout = 30.seconds) {
config {
engine {
requestTimeout = 0
}
}

val body = "Hello World\n"

withServerSocket { client, socket ->
sendExpectRequest(socket, client, body) {
assertEquals(HttpStatusCode.OK, status)
}

socket.accept().use {
val readChannel = it.openReadChannel()
val writeChannel = it.openWriteChannel()

val headers = readAvailableLines(readChannel)
delay(2000)
val actualBody = readAvailableLine(readChannel)
assertTrue(headers.contains(EXPECT_HEADER))
assertEquals(body, actualBody)
writeOkResponse(writeChannel)
}
}
}

@Test
fun testRepeatRequestAfterExpectationFailed() = runTestWithRealTime {
fun testRepeatRequestAfterExpectationFailed() = testClient {
val body = "Hello World"

withServerSocket { socket ->
val client = HttpClient(CIO)
launch {
sendExpectRequest(socket, client, body).apply {
assertEquals(HttpStatusCode.OK, status)
}
withServerSocket { client, socket ->
sendExpectRequest(socket, client, body) {
assertEquals(HttpStatusCode.OK, status)
}

socket.accept().use {
Expand All @@ -178,7 +172,7 @@ class CIOEngineTest {
}

@Test
fun testErrorMessageWhenServerDontRespondWithUpgrade() = testWithEngine(CIO) {
fun testErrorMessageWhenServerDontRespondWithUpgrade() = testClient {
config {
install(WebSockets)
}
Expand All @@ -192,13 +186,21 @@ class CIOEngineTest {
}
}

private suspend fun sendExpectRequest(socket: ServerSocket, client: HttpClient, body: String? = null) =
client.post {
val serverPort = (socket.localAddress as InetSocketAddress).port
url(host = TEST_SERVER_SOCKET_HOST, port = serverPort, path = "/")
header(HttpHeaders.Expect, "100-continue")
if (body != null) setBody(body)
private fun CoroutineScope.sendExpectRequest(
socket: ServerSocket,
client: HttpClient,
body: String? = null,
block: HttpResponse.() -> Unit = {}
) {
launch {
client.post {
val serverPort = (socket.localAddress as InetSocketAddress).port
url(host = TEST_SERVER_SOCKET_HOST, port = serverPort, path = "/")
header(HttpHeaders.Expect, "100-continue")
if (body != null) setBody(body)
}.apply(block)
}
}

private suspend fun readAvailableLine(channel: ByteReadChannel): String {
val buffer = ByteArray(1024)
Expand Down Expand Up @@ -236,10 +238,12 @@ class CIOEngineTest {
}
}

private suspend fun withServerSocket(block: suspend (ServerSocket) -> Unit) {
private fun TestClientBuilder<*>.withServerSocket(
block: suspend CoroutineScope.(HttpClient, ServerSocket) -> Unit,
) = test { client ->
selectorManager.use {
aSocket(it).tcp().bind(TEST_SERVER_SOCKET_HOST, 0).use { socket ->
block(socket)
block(client, socket)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.cio
Expand All @@ -9,6 +9,7 @@ import io.ktor.client.network.sockets.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.client.test.base.*
import io.ktor.client.tests.utils.*
import io.ktor.http.*
import io.ktor.http.content.*
Expand Down
Loading

0 comments on commit c47dfda

Please sign in to comment.