From dbcbff9659c92b6416d04cb2a9c92d10bcd2d149 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Tue, 17 Dec 2024 15:19:19 +0100 Subject: [PATCH] Cherry-pick common ClientLoader implementation from 3.1.0-eap (#4550) * Add nonJvm source set * Commonize ClientLoader and make it work with multiple engines on js/wasmJs --------- Co-authored-by: Oleg Yukhnevich --- buildSrc/src/main/kotlin/TargetsConfig.kt | 14 +- .../io/ktor/client/plugins/auth/AuthTest.kt | 8 +- .../ktor/client/tests/utils/ClientLoader.kt | 116 +++++++++++++- .../tests/utils/CommonClientTestUtils.kt | 15 +- .../io/ktor/client/tests/BodyProgressTest.kt | 2 +- .../test/io/ktor/client/tests/ContentTest.kt | 7 +- .../test/io/ktor/client/tests/EventsTest.kt | 2 +- .../ktor/client/tests/HttpRequestRetryTest.kt | 2 - .../io/ktor/client/tests/HttpTimeoutTest.kt | 16 +- .../test/io/ktor/client/tests/JsonTest.kt | 2 +- .../test/io/ktor/client/tests/LoggingTest.kt | 17 +- .../client/tests/MultiPartFormDataTest.kt | 2 +- .../io/ktor/client/tests/WebSocketTest.kt | 2 +- .../tests/plugins/CacheLegacyStorageTest.kt | 2 +- .../tests/plugins/CookiesIntegrationTests.kt | 2 +- .../tests/plugins/ServerSentEventsTest.kt | 11 +- .../ktor/client/tests/utils/ClientLoaderJs.kt | 36 ----- .../utils/ClientLoader.jsAndWasmShared.kt | 11 ++ .../client/tests/utils/ClientLoader.jvm.kt | 82 ++++++++++ .../client/tests/utils/ClientLoaderJvm.kt | 151 ------------------ .../io/ktor/client/tests/LoggingTestJvm.kt | 13 +- .../io/ktor/client/tests/MultithreadedTest.kt | 14 +- .../io/ktor/client/tests/WebSocketJvmTest.kt | 3 +- .../client/tests/utils/ClientLoader.nonJvm.kt | 9 ++ .../client/tests/utils/ClientLoader.posix.kt | 12 ++ .../client/tests/utils/ClientLoaderNative.kt | 69 -------- .../client/tests/utils/ClientLoaderWasm.kt | 36 ----- 27 files changed, 308 insertions(+), 348 deletions(-) delete mode 100644 ktor-client/ktor-client-tests/js/src/io/ktor/client/tests/utils/ClientLoaderJs.kt create mode 100644 ktor-client/ktor-client-tests/jsAndWasmShared/src/io/ktor/client/tests/utils/ClientLoader.jsAndWasmShared.kt create mode 100644 ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientLoader.jvm.kt delete mode 100644 ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientLoaderJvm.kt create mode 100644 ktor-client/ktor-client-tests/nonJvm/src/io/ktor/client/tests/utils/ClientLoader.nonJvm.kt create mode 100644 ktor-client/ktor-client-tests/posix/src/io/ktor/client/tests/utils/ClientLoader.posix.kt delete mode 100644 ktor-client/ktor-client-tests/posix/src/io/ktor/client/tests/utils/ClientLoaderNative.kt delete mode 100644 ktor-client/ktor-client-tests/wasmJs/src/io/ktor/client/tests/utils/ClientLoaderWasm.kt diff --git a/buildSrc/src/main/kotlin/TargetsConfig.kt b/buildSrc/src/main/kotlin/TargetsConfig.kt index 2293c5b70f8..3780f03a3c5 100644 --- a/buildSrc/src/main/kotlin/TargetsConfig.kt +++ b/buildSrc/src/main/kotlin/TargetsConfig.kt @@ -14,17 +14,18 @@ import java.io.* private val Project.files: Array get() = project.projectDir.listFiles() ?: emptyArray() val Project.hasCommon: Boolean get() = files.any { it.name == "common" } +val Project.hasNonJvm: Boolean get() = files.any { it.name == "nonJvm" } val Project.hasJvmAndPosix: Boolean get() = hasCommon || files.any { it.name == "jvmAndPosix" } -val Project.hasPosix: Boolean get() = hasCommon || hasJvmAndPosix || files.any { it.name == "posix" } +val Project.hasPosix: Boolean get() = hasCommon || hasNonJvm || hasJvmAndPosix || files.any { it.name == "posix" } val Project.hasDesktop: Boolean get() = hasPosix || files.any { it.name == "desktop" } val Project.hasNix: Boolean get() = hasPosix || files.any { it.name == "nix" } val Project.hasLinux: Boolean get() = hasNix || files.any { it.name == "linux" } val Project.hasDarwin: Boolean get() = hasNix || files.any { it.name == "darwin" } val Project.hasAndroidNative: Boolean get() = hasPosix || files.any { it.name == "androidNative" } val Project.hasWindows: Boolean get() = hasPosix || files.any { it.name == "windows" } -val Project.hasJsAndWasmShared: Boolean get() = files.any { it.name == "jsAndWasmShared" } -val Project.hasJs: Boolean get() = hasCommon || files.any { it.name == "js" } || hasJsAndWasmShared -val Project.hasWasmJs: Boolean get() = hasCommon || files.any { it.name == "wasmJs" } || hasJsAndWasmShared +val Project.hasJsAndWasmShared: Boolean get() = hasCommon || hasNonJvm || files.any { it.name == "jsAndWasmShared" } +val Project.hasJs: Boolean get() = hasJsAndWasmShared || files.any { it.name == "js" } +val Project.hasWasmJs: Boolean get() = hasJsAndWasmShared || files.any { it.name == "wasmJs" } val Project.hasJvm: Boolean get() = hasCommon || hasJvmAndPosix || files.any { it.name == "jvm" } val Project.hasExplicitNative: Boolean @@ -114,6 +115,11 @@ private val hierarchyTemplate = KotlinHierarchyTemplate { group("windows") group("macos") } + + group("nonJvm") { + group("posix") + group("jsAndWasmShared") + } } } diff --git a/ktor-client/ktor-client-plugins/ktor-client-auth/common/test/io/ktor/client/plugins/auth/AuthTest.kt b/ktor-client/ktor-client-plugins/ktor-client-auth/common/test/io/ktor/client/plugins/auth/AuthTest.kt index 8809eb5f08a..828573ec60f 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-auth/common/test/io/ktor/client/plugins/auth/AuthTest.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-auth/common/test/io/ktor/client/plugins/auth/AuthTest.kt @@ -23,7 +23,7 @@ import kotlin.test.assertFailsWith class AuthTest : ClientLoader() { @Test - fun testDigestAuthLegacy() = clientTests(listOf("Js", "native")) { + fun testDigestAuthLegacy() = clientTests(listOf("Js", "native:*")) { config { install(Auth) { digest { @@ -43,7 +43,7 @@ class AuthTest : ClientLoader() { } @Test - fun testDigestAuth() = clientTests(listOf("Js", "native")) { + fun testDigestAuth() = clientTests(listOf("Js", "native:*")) { config { install(Auth) { digest { @@ -60,7 +60,7 @@ class AuthTest : ClientLoader() { } @Test - fun testDigestAuthPerRealm() = clientTests(listOf("Js", "native")) { + fun testDigestAuthPerRealm() = clientTests(listOf("Js", "native:*")) { config { install(Auth) { digest { @@ -84,7 +84,7 @@ class AuthTest : ClientLoader() { } @Test - fun testDigestAuthSHA256() = clientTests(listOf("Js", "native")) { + fun testDigestAuthSHA256() = clientTests(listOf("Js", "native:*")) { config { install(Auth) { digest { diff --git a/ktor-client/ktor-client-tests/common/src/io/ktor/client/tests/utils/ClientLoader.kt b/ktor-client/ktor-client-tests/common/src/io/ktor/client/tests/utils/ClientLoader.kt index ecd84cc776c..a4464477a44 100644 --- a/ktor-client/ktor-client-tests/common/src/io/ktor/client/tests/utils/ClientLoader.kt +++ b/ktor-client/ktor-client-tests/common/src/io/ktor/client/tests/utils/ClientLoader.kt @@ -5,12 +5,20 @@ package io.ktor.client.tests.utils import io.ktor.client.engine.* -import kotlinx.coroutines.test.* +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.runTest +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +internal expect val enginesToTest: Iterable> +internal expect val platformName: String +internal expect fun platformDumpCoroutines() +internal expect fun platformWaitForAllCoroutines() /** * Helper interface to test client. */ -expect abstract class ClientLoader(timeoutSeconds: Int = 60) { +abstract class ClientLoader(private val timeout: Duration = 1.minutes) { /** * Perform test against all clients from dependencies. */ @@ -19,10 +27,110 @@ expect abstract class ClientLoader(timeoutSeconds: Int = 60) { onlyWithEngine: String? = null, retries: Int = 1, block: suspend TestClientBuilder.() -> Unit - ): TestResult + ): TestResult = runTest(timeout = timeout) { + val skipPatterns = skipEngines.map(SkipEnginePattern::parse) + + val failures: List = enginesToTest.mapNotNull { engineFactory -> + val engineName = engineFactory.engineName + + if (shouldRun(engineName, skipPatterns, onlyWithEngine)) { + try { + println("Run test with engine $engineName") + // run test here + performTestWithEngine(engineFactory, this@ClientLoader, retries, block) + null // engine test passed + } catch (cause: Throwable) { + // engine test failed, save failure to report after run for every engine. + TestFailure(engineName, cause) + } + } else { + println("Skipping test with engine $engineName") + null // engine skipped + } + } + + if (failures.isNotEmpty()) { + val message = buildString { + appendLine("Test failed for engines: ${failures.map { it.engineName }}") + failures.forEach { + appendLine("Test failed for engine '$platformName:${it.engineName}' with:") + appendLine(it.cause.stackTraceToString().prependIndent(" ")) + } + } + throw AssertionError(message) + } + } + + private fun shouldRun( + engineName: String, + skipEnginePatterns: List, + onlyWithEngine: String? + ): Boolean { + val lowercaseEngineName = engineName.lowercase() + if (onlyWithEngine != null && onlyWithEngine.lowercase() != lowercaseEngineName) return false + + skipEnginePatterns.forEach { + if (it.matches(lowercaseEngineName)) return false + } + + return true + } /** * Print coroutines in debug mode. */ - fun dumpCoroutines() + fun dumpCoroutines(): Unit = platformDumpCoroutines() + + // Issues to fix before unlocking: + // 1. Pinger & Ponger in ws + // 2. Nonce generator + // @After + fun waitForAllCoroutines(): Unit = platformWaitForAllCoroutines() } + +internal val HttpClientEngineFactory<*>.engineName: String + get() = this::class.simpleName!! + +private data class SkipEnginePattern( + val skippedPlatform: String?, // null means * or empty + val skippedEngine: String?, // null means * or empty +) { + fun matches(engineName: String): Boolean { + var result = true + if (skippedEngine != null) { + result = result && engineName == skippedEngine + } + if (result && skippedPlatform != null) { + result = result && platformName.startsWith(skippedPlatform) + } + return result + } + + companion object { + fun parse(pattern: String): SkipEnginePattern { + val parts = pattern.lowercase().split(":").map { it.takeIf { it != "*" } } + val platform: String? + val engine: String? + when (parts.size) { + 1 -> { + platform = null + engine = parts[0] + } + + 2 -> { + platform = parts[0] + engine = parts[1] + } + + else -> error("Skip engine pattern should consist of two parts: PLATFORM:ENGINE or ENGINE") + } + + if (platform == null && engine == null) { + error("Skip engine pattern should consist of two parts: PLATFORM:ENGINE or ENGINE") + } + return SkipEnginePattern(platform, engine) + } + } +} + +private class TestFailure(val engineName: String, val cause: Throwable) diff --git a/ktor-client/ktor-client-tests/common/src/io/ktor/client/tests/utils/CommonClientTestUtils.kt b/ktor-client/ktor-client-tests/common/src/io/ktor/client/tests/utils/CommonClientTestUtils.kt index 1dd64ca5266..25012cd454e 100644 --- a/ktor-client/ktor-client-tests/common/src/io/ktor/client/tests/utils/CommonClientTestUtils.kt +++ b/ktor-client/ktor-client-tests/common/src/io/ktor/client/tests/utils/CommonClientTestUtils.kt @@ -2,15 +2,13 @@ * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("NO_EXPLICIT_RETURN_TYPE_IN_API_MODE_WARNING", "KDocMissingDocumentation") - package io.ktor.client.tests.utils import io.ktor.client.* import io.ktor.client.engine.* import io.ktor.utils.io.core.* import kotlinx.coroutines.* -import kotlinx.coroutines.test.* +import kotlinx.coroutines.test.runTest import kotlin.time.Duration.Companion.milliseconds /** @@ -65,7 +63,6 @@ private fun testWithClient( /** * Perform test with selected client engine [factory]. */ -@OptIn(DelicateCoroutinesApi::class) fun testWithEngine( factory: HttpClientEngineFactory, loader: ClientLoader? = null, @@ -73,6 +70,16 @@ fun testWithEngine( retries: Int = 1, block: suspend TestClientBuilder.() -> Unit ) = runTest(timeout = timeoutMillis.milliseconds) { + performTestWithEngine(factory, loader, retries, block) +} + +@OptIn(DelicateCoroutinesApi::class) +suspend fun performTestWithEngine( + factory: HttpClientEngineFactory, + loader: ClientLoader? = null, + retries: Int = 1, + block: suspend TestClientBuilder.() -> Unit +) { val builder = TestClientBuilder().apply { block() } if (builder.dumpAfterDelay > 0 && loader != null) { diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/BodyProgressTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/BodyProgressTest.kt index c67f33eaaf5..7f0723b1137 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/BodyProgressTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/BodyProgressTest.kt @@ -33,7 +33,7 @@ private val TEST_ARRAY = ByteArray(8 * 1025) { 1 } private val TEST_NAME = "123".repeat(5000) @OptIn(DelicateCoroutinesApi::class) -class BodyProgressTest : ClientLoader(timeoutSeconds = 60) { +class BodyProgressTest : ClientLoader() { @Serializable data class User(val login: String, val id: Long) diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentTest.kt index 758e62889a8..cbdc7f5648d 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentTest.kt @@ -18,8 +18,9 @@ import io.ktor.http.content.* import io.ktor.serialization.kotlinx.json.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* -import kotlinx.coroutines.* -import kotlinx.io.* +import kotlinx.coroutines.cancel +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.io.readByteArray import kotlin.test.* import kotlin.time.Duration.Companion.minutes @@ -42,7 +43,7 @@ val testArrays = testSize.map { makeArray(it) } -class ContentTest : ClientLoader(5 * 60) { +class ContentTest : ClientLoader(timeout = 5.minutes) { @Test fun testGetFormData() = clientTests { diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/EventsTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/EventsTest.kt index 55195d6d085..fbf9b12ff05 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/EventsTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/EventsTest.kt @@ -74,7 +74,7 @@ class EventsTest : ClientLoader() { } @Test - fun testRedirectEvent() = clientTests(listOf("js")) { + fun testRedirectEvent() = clientTests(listOf("Js")) { test { client -> counter.value = 0 client.monitor.subscribe(HttpResponseRedirectEvent) { diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpRequestRetryTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpRequestRetryTest.kt index 22adce9bd44..9d326cc2257 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpRequestRetryTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpRequestRetryTest.kt @@ -9,10 +9,8 @@ import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.tests.utils.* import io.ktor.http.* -import io.ktor.utils.io.errors.* import kotlinx.coroutines.* import kotlinx.io.IOException -import kotlin.math.* import kotlin.test.* class HttpRequestRetryTest { diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpTimeoutTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpTimeoutTest.kt index 9285dd2a14b..6618c91a8ca 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpTimeoutTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/HttpTimeoutTest.kt @@ -327,7 +327,7 @@ class HttpTimeoutTest : ClientLoader() { // Fix https://youtrack.jetbrains.com/issue/KTOR-7885 @Ignore @Test - fun testRedirect() = clientTests(listOf("js"), retries = 5) { + fun testRedirect() = clientTests(listOf("Js"), retries = 5) { config { install(HttpTimeout) { requestTimeoutMillis = 10000 } } @@ -344,7 +344,7 @@ class HttpTimeoutTest : ClientLoader() { // Js can't configure test timeout in browser @Test - fun testRedirectPerRequestAttributes() = clientTests(listOf("js")) { + fun testRedirectPerRequestAttributes() = clientTests(listOf("Js")) { config { install(HttpTimeout) } @@ -429,7 +429,7 @@ class HttpTimeoutTest : ClientLoader() { } @Test - fun testConnectionRefusedException() = clientTests(listOf("Js", "native:*", "win:*")) { + fun testConnectionRefusedException() = clientTests(listOf("Js", "native:*", "jvm/win:*")) { config { install(HttpTimeout) { connectTimeoutMillis = 1000 } } @@ -445,7 +445,7 @@ class HttpTimeoutTest : ClientLoader() { } @Test - fun testSocketTimeoutRead() = clientTests(listOf("Js", "native:CIO", "Java")) { + fun testSocketTimeoutRead() = clientTests(listOf("Js", "native:CIO", "Curl", "Java")) { config { install(HttpTimeout) { socketTimeoutMillis = 1000 } } @@ -460,7 +460,9 @@ class HttpTimeoutTest : ClientLoader() { } @Test - fun testSocketTimeoutReadPerRequestAttributes() = clientTests(listOf("Js", "native:CIO", "Java", "Apache5")) { + fun testSocketTimeoutReadPerRequestAttributes() = clientTests( + listOf("Js", "native:CIO", "Curl", "Java", "Apache5") + ) { config { install(HttpTimeout) } @@ -477,7 +479,7 @@ class HttpTimeoutTest : ClientLoader() { } @Test - fun testSocketTimeoutWriteFailOnWrite() = clientTests(listOf("Js", "Android", "native:CIO", "Java")) { + fun testSocketTimeoutWriteFailOnWrite() = clientTests(listOf("Js", "Android", "native:CIO", "Curl", "Java")) { config { install(HttpTimeout) { socketTimeoutMillis = 500 } } @@ -491,7 +493,7 @@ class HttpTimeoutTest : ClientLoader() { @Test fun testSocketTimeoutWriteFailOnWritePerRequestAttributes() = clientTests( - listOf("Js", "Android", "Apache5", "native:CIO", "Java") + listOf("Js", "Android", "Apache5", "native:CIO", "Curl", "Java") ) { config { install(HttpTimeout) diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/JsonTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/JsonTest.kt index f3c983e0f48..e93fc3c8ddc 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/JsonTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/JsonTest.kt @@ -21,7 +21,7 @@ class JsonTest : ClientLoader() { data class Result(val message: String, val data: T) @Test - fun testUserGenerics() = clientTests(listOf("js")) { + fun testUserGenerics() = clientTests(listOf("Js")) { config { install(ContentNegotiation) { json() } } diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingTest.kt index 91cd602be0a..97cc96d7e02 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingTest.kt @@ -1,6 +1,6 @@ /* -* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. -*/ + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ package io.ktor.client.tests @@ -16,9 +16,14 @@ import io.ktor.client.tests.utils.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.utils.io.* -import kotlinx.coroutines.* -import kotlinx.serialization.* -import kotlin.test.* +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue @OptIn(DelicateCoroutinesApi::class) class LoggingTest : ClientLoader() { @@ -352,7 +357,7 @@ class LoggingTest : ClientLoader() { } @Test - fun testLoggingWithCompression() = clientTests(listOf("native:CIO")) { + fun testLoggingWithCompression() = clientTests(listOf("Darwin", "DarwinLegacy", "native:CIO")) { val testLogger = TestLogger( "REQUEST: http://127.0.0.1:8080/compression/deflate", "METHOD: HttpMethod(value=GET)", diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/MultiPartFormDataTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/MultiPartFormDataTest.kt index 74cc70ebca7..462c2b552c7 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/MultiPartFormDataTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/MultiPartFormDataTest.kt @@ -25,7 +25,7 @@ class MultiPartFormDataTest : ClientLoader() { } @Test - fun testMultiPartFormData() = clientTests(listOf("native")) { + fun testMultiPartFormData() = clientTests(listOf("native:*")) { test { client -> val result = client.preparePost("$TEST_SERVER/multipart") { setBody( diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/WebSocketTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/WebSocketTest.kt index 21fe8656afa..144970bddb2 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/WebSocketTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/WebSocketTest.kt @@ -329,7 +329,7 @@ class WebSocketTest : ClientLoader() { @Ignore // TODO KTOR-7088 @Test fun testImmediateReceiveAfterConnect() = clientTests( - ENGINES_WITHOUT_WS + "Darwin" + "js" // TODO KTOR-7088 + ENGINES_WITHOUT_WS + "Darwin" + "Js" // TODO KTOR-7088 ) { config { install(WebSockets) diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CacheLegacyStorageTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CacheLegacyStorageTest.kt index 536c3b38e2b..5e3f7f26c45 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CacheLegacyStorageTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CacheLegacyStorageTest.kt @@ -569,7 +569,7 @@ class CacheLegacyStorageTest : ClientLoader() { } @Test - fun testPublicAndPrivateCache() = clientTests(listOf("native")) { + fun testPublicAndPrivateCache() = clientTests(listOf("native:*")) { val publicStorage = HttpCacheStorage.Unlimited() val privateStorage = HttpCacheStorage.Unlimited() config { diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CookiesIntegrationTests.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CookiesIntegrationTests.kt index 33362690ddd..62208972750 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CookiesIntegrationTests.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CookiesIntegrationTests.kt @@ -106,7 +106,7 @@ class CookiesIntegrationTests : ClientLoader() { } @Test - fun testPath() = clientTests(listOf("js")) { + fun testPath() = clientTests(listOf("Js")) { config { install(HttpCookies) } diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/ServerSentEventsTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/ServerSentEventsTest.kt index 5d3fcfc5229..4dd4e669640 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/ServerSentEventsTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/ServerSentEventsTest.kt @@ -19,12 +19,15 @@ import io.ktor.test.dispatcher.* import io.ktor.utils.io.* import io.ktor.utils.io.charsets.* import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlin.coroutines.* +import kotlinx.coroutines.flow.collectIndexed +import kotlinx.coroutines.flow.single +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import kotlin.test.* -import kotlin.test.assertFailsWith +import kotlin.time.Duration.Companion.minutes -class ServerSentEventsTest : ClientLoader(timeoutSeconds = 120) { +class ServerSentEventsTest : ClientLoader(timeout = 2.minutes) { @Test fun testExceptionIfSseIsNotInstalled() = testSuspend { diff --git a/ktor-client/ktor-client-tests/js/src/io/ktor/client/tests/utils/ClientLoaderJs.kt b/ktor-client/ktor-client-tests/js/src/io/ktor/client/tests/utils/ClientLoaderJs.kt deleted file mode 100644 index 656d8ba9950..00000000000 --- a/ktor-client/ktor-client-tests/js/src/io/ktor/client/tests/utils/ClientLoaderJs.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package io.ktor.client.tests.utils - -import io.ktor.client.engine.* -import io.ktor.client.engine.js.* -import kotlinx.coroutines.test.TestResult -import kotlinx.coroutines.test.runTest - -/** - * Helper interface to test client. - */ -actual abstract class ClientLoader actual constructor(private val timeoutSeconds: Int) { - /** - * Perform test against all clients from dependencies. - */ - actual fun clientTests( - skipEngines: List, - onlyWithEngine: String?, - retries: Int, - block: suspend TestClientBuilder.() -> Unit - ): TestResult { - val skipEnginesLowerCase = skipEngines.map { it.lowercase() } - if ((onlyWithEngine != null && onlyWithEngine != "js") || skipEnginesLowerCase.contains("js")) { - return runTest { } - } - - return testWithEngine(Js, retries = retries, timeoutMillis = timeoutSeconds * 1000L, block = block) - } - - actual fun dumpCoroutines() { - error("Debug probes unsupported[js]") - } -} diff --git a/ktor-client/ktor-client-tests/jsAndWasmShared/src/io/ktor/client/tests/utils/ClientLoader.jsAndWasmShared.kt b/ktor-client/ktor-client-tests/jsAndWasmShared/src/io/ktor/client/tests/utils/ClientLoader.jsAndWasmShared.kt new file mode 100644 index 00000000000..4c03e746f53 --- /dev/null +++ b/ktor-client/ktor-client-tests/jsAndWasmShared/src/io/ktor/client/tests/utils/ClientLoader.jsAndWasmShared.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.client.tests.utils + +import io.ktor.client.engine.* +import io.ktor.client.engine.js.* + +internal actual val enginesToTest: Iterable> get() = listOf(Js) +internal actual val platformName: String get() = "web" diff --git a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientLoader.jvm.kt b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientLoader.jvm.kt new file mode 100644 index 00000000000..fd6af1cc1f4 --- /dev/null +++ b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientLoader.jvm.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.client.tests.utils + +import io.ktor.client.* +import io.ktor.client.engine.* +import io.ktor.util.reflect.* +import io.ktor.utils.io.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.debug.CoroutineInfo +import kotlinx.coroutines.debug.DebugProbes +import java.util.* + +@OptIn(InternalAPI::class) +internal actual val enginesToTest: Iterable> by lazy { + val enginesIterator = loadServicesAsSequence().iterator() + + buildList { + while (enginesIterator.hasNext()) { + try { + add(enginesIterator.next().factory) + } catch (_: UnsupportedClassVersionError) { + // Ignore clients compiled against newer JVM + } + } + } +} + +internal actual val platformName: String by lazy { + val os = System.getProperty("os.name", "unknown").lowercase(Locale.getDefault()) + "jvm/" + when { + os.contains("win") -> "win" + os.contains("mac") -> "mac" + os.contains("nux") -> "unix" + else -> "unknown" + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +internal actual fun platformDumpCoroutines() { + DebugProbes.dumpCoroutines() + + println("Thread Dump") + Thread.getAllStackTraces().forEach { (thread, stackTrace) -> + println("Thread: $thread") + stackTrace.forEach { + println("\t$it") + } + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +internal actual fun platformWaitForAllCoroutines() { + check(DebugProbes.isInstalled) { + "Debug probes isn't installed." + } + + val info = DebugProbes.dumpCoroutinesInfo() + + if (info.isEmpty()) { + return + } + + val message = buildString { + appendLine("Test failed. There are running coroutines") + appendLine(info.dump()) + } + + error(message) +} + +@OptIn(ExperimentalCoroutinesApi::class) +private fun List.dump(): String = buildString { + this@dump.forEach { info -> + appendLine("Coroutine: $info") + info.lastObservedStackTrace().forEach { + appendLine("\t$it") + } + } +} diff --git a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientLoaderJvm.kt b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientLoaderJvm.kt deleted file mode 100644 index d3244e777cb..00000000000 --- a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientLoaderJvm.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package io.ktor.client.tests.utils - -import io.ktor.client.* -import io.ktor.client.engine.* -import io.ktor.util.reflect.* -import io.ktor.utils.io.* -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.debug.CoroutineInfo -import kotlinx.coroutines.debug.DebugProbes -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import java.util.* -import kotlin.time.Duration.Companion.seconds - -/** - * Helper interface to test client. - */ -actual abstract class ClientLoader actual constructor(val timeoutSeconds: Int) { - - private val engines: List - get() = supportedEngines - - /** - * Perform test against all clients from dependencies. - */ - @OptIn(ExperimentalCoroutinesApi::class) - actual fun clientTests( - skipEngines: List, - onlyWithEngine: String?, - retries: Int, - block: suspend TestClientBuilder.() -> Unit - ) { - DebugProbes.install() - for (engine in engines) { - if (shouldSkip(engine, skipEngines, onlyWithEngine)) { - continue - } - runBlocking { - withTimeout(timeoutSeconds.seconds.inWholeMilliseconds) { - testWithEngine(engine.factory, this@ClientLoader, timeoutSeconds * 1000L, retries, block) - } - } - } - } - - private fun shouldSkip( - engine: HttpClientEngineContainer, - skipEngines: List, - onlyWithEngine: String? - ): Boolean { - val engineName = engine.toString() - return (onlyWithEngine != null && !onlyWithEngine.equals(engineName, ignoreCase = true)) || - skipEngines.any { shouldSkip(engineName, it) } - } - - private fun shouldSkip(engineName: String, skipEngine: String): Boolean { - val locale = Locale.getDefault() - val skipEngineArray = skipEngine.lowercase(locale).split(":") - - val (platform, skipEngineName) = when (skipEngineArray.size) { - 2 -> skipEngineArray[0] to skipEngineArray[1] - 1 -> "*" to skipEngineArray[0] - else -> throw IllegalStateException("Wrong skip engine format, expected 'engine' or 'platform:engine'") - } - - val platformShouldBeSkipped = "*" == platform || OS_NAME == platform - val engineShouldBeSkipped = "*" == skipEngineName || - engineName.lowercase(locale) == skipEngineName.lowercase(locale) - - return engineShouldBeSkipped && platformShouldBeSkipped - } - - @OptIn(ExperimentalCoroutinesApi::class) - actual fun dumpCoroutines() { - DebugProbes.dumpCoroutines() - - println("Thread Dump") - Thread.getAllStackTraces().forEach { (thread, stackTrace) -> - println("Thread: $thread") - stackTrace.forEach { - println("\t$it") - } - } - } - - /** - * Issues to fix before unlock: - * 1. Pinger & Ponger in ws - * 2. Nonce generator - * Then @After should be added to the function - */ - @OptIn(ExperimentalCoroutinesApi::class) - fun waitForAllCoroutines() { - check(DebugProbes.isInstalled) { - "Debug probes isn't installed." - } - - val info = DebugProbes.dumpCoroutinesInfo() - - if (info.isEmpty()) { - return - } - - val message = buildString { - appendLine("Test failed. There are running coroutines") - appendLine(info.dump()) - } - - error(message) - } -} - -@OptIn(InternalAPI::class) -private val supportedEngines by lazy { - val enginesIterator = loadServicesAsSequence().iterator() - - buildList { - while (enginesIterator.hasNext()) { - try { - add(enginesIterator.next()) - } catch (_: UnsupportedClassVersionError) { - // Ignore clients compiled against newer JVM - } - } - } -} - -private val OS_NAME: String - get() { - val os = System.getProperty("os.name", "unknown").lowercase(Locale.getDefault()) - return when { - os.contains("win") -> "win" - os.contains("mac") -> "mac" - os.contains("nux") -> "unix" - else -> "unknown" - } - } - -@OptIn(ExperimentalCoroutinesApi::class) -private fun List.dump(): String = buildString { - this@dump.forEach { info -> - appendLine("Coroutine: $info") - info.lastObservedStackTrace().forEach { - appendLine("\t$it") - } - } -} diff --git a/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/LoggingTestJvm.kt b/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/LoggingTestJvm.kt index 8862a94c1b9..4fdfd102266 100644 --- a/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/LoggingTestJvm.kt +++ b/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/LoggingTestJvm.kt @@ -5,15 +5,16 @@ package io.ktor.client.tests import io.ktor.client.plugins.logging.* -import io.ktor.client.plugins.logging.Logger import io.ktor.client.request.* import io.ktor.client.tests.utils.* import io.ktor.http.* import io.ktor.utils.io.* -import kotlinx.coroutines.* -import kotlinx.coroutines.slf4j.* -import org.slf4j.* -import kotlin.test.* +import kotlinx.coroutines.withContext +import org.slf4j.MDC +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds private class LoggerWithMdc : Logger { val logs = mutableListOf>() @@ -27,7 +28,7 @@ private class LoggerWithMdc : Logger { } } -class LoggingTestJvm : ClientLoader(timeoutSeconds = 1000000) { +class LoggingTestJvm : ClientLoader(timeout = 1000000.seconds) { @OptIn(InternalAPI::class) @Test diff --git a/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/MultithreadedTest.kt b/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/MultithreadedTest.kt index e6caa8229a0..ec384350ecb 100644 --- a/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/MultithreadedTest.kt +++ b/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/MultithreadedTest.kt @@ -7,14 +7,20 @@ package io.ktor.client.tests import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.client.tests.utils.* -import kotlinx.coroutines.* -import java.util.concurrent.* -import kotlin.test.* +import kotlinx.coroutines.runBlocking +import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes private const val TEST_SIZE = 100_000 private const val DEFAULT_THREADS_COUNT = 32 -class MultithreadedTest : ClientLoader(timeoutSeconds = 10 * 60) { +class MultithreadedTest : ClientLoader(timeout = 10.minutes) { @Test @Ignore fun numberTest() = clientTests { diff --git a/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/WebSocketJvmTest.kt b/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/WebSocketJvmTest.kt index af98b6a2aff..bb7fa124340 100644 --- a/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/WebSocketJvmTest.kt +++ b/ktor-client/ktor-client-tests/jvm/test/io/ktor/client/tests/WebSocketJvmTest.kt @@ -8,10 +8,11 @@ import io.ktor.client.plugins.websocket.* import io.ktor.client.tests.utils.* import io.ktor.websocket.* import kotlin.test.* +import kotlin.time.Duration.Companion.seconds private const val TEST_SIZE: Int = 100 -class WebSocketJvmTest : ClientLoader(100000) { +class WebSocketJvmTest : ClientLoader(100000.seconds) { @Test fun testWebSocketDeflateBinary() = clientTests(listOf("Android", "Apache", "Apache5")) { diff --git a/ktor-client/ktor-client-tests/nonJvm/src/io/ktor/client/tests/utils/ClientLoader.nonJvm.kt b/ktor-client/ktor-client-tests/nonJvm/src/io/ktor/client/tests/utils/ClientLoader.nonJvm.kt new file mode 100644 index 00000000000..4ea4cbf9b6b --- /dev/null +++ b/ktor-client/ktor-client-tests/nonJvm/src/io/ktor/client/tests/utils/ClientLoader.nonJvm.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.client.tests.utils + +// supported only on JVM +internal actual fun platformDumpCoroutines() {} +internal actual fun platformWaitForAllCoroutines() {} diff --git a/ktor-client/ktor-client-tests/posix/src/io/ktor/client/tests/utils/ClientLoader.posix.kt b/ktor-client/ktor-client-tests/posix/src/io/ktor/client/tests/utils/ClientLoader.posix.kt new file mode 100644 index 00000000000..ecbd5ce5a58 --- /dev/null +++ b/ktor-client/ktor-client-tests/posix/src/io/ktor/client/tests/utils/ClientLoader.posix.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.client.tests.utils + +import io.ktor.client.engine.* +import io.ktor.utils.io.* + +@OptIn(InternalAPI::class) +internal actual val enginesToTest: Iterable> get() = engines +internal actual val platformName: String get() = "native" diff --git a/ktor-client/ktor-client-tests/posix/src/io/ktor/client/tests/utils/ClientLoaderNative.kt b/ktor-client/ktor-client-tests/posix/src/io/ktor/client/tests/utils/ClientLoaderNative.kt deleted file mode 100644 index 153ee6cb6f9..00000000000 --- a/ktor-client/ktor-client-tests/posix/src/io/ktor/client/tests/utils/ClientLoaderNative.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package io.ktor.client.tests.utils - -import io.ktor.client.engine.* -import io.ktor.utils.io.* -import kotlin.experimental.* - -private class TestFailure(val name: String, val cause: Throwable) { - @OptIn(ExperimentalNativeApi::class) - override fun toString(): String = buildString { - appendLine("Test failed with engine: $name") - appendLine(cause) - for (stackline in cause.getStackTrace()) { - appendLine("\t$stackline") - } - } -} - -/** - * Helper interface to test client. - */ -actual abstract class ClientLoader actual constructor(private val timeoutSeconds: Int) { - /** - * Perform test against all clients from dependencies. - */ - @OptIn(InternalAPI::class) - actual fun clientTests( - skipEngines: List, - onlyWithEngine: String?, - retries: Int, - block: suspend TestClientBuilder.() -> Unit - ) { - if (skipEngines.any { it.startsWith("native") }) return - - val skipEnginesLowerCase = skipEngines.map { it.lowercase() }.toSet() - val filteredEngines: List> = engines.filter { - val name = it.toString().lowercase() - !skipEnginesLowerCase.contains(name) && !skipEnginesLowerCase.contains("native:$name") - } - - val failures = mutableListOf() - for (engine in filteredEngines) { - if (onlyWithEngine != null && onlyWithEngine != engine.toString()) continue - - val result = runCatching { - testWithEngine(engine, timeoutMillis = timeoutSeconds.toLong() * 1000L) { - block() - } - } - - if (result.isFailure) { - failures += TestFailure(engine.toString(), result.exceptionOrNull()!!) - } - } - - if (failures.isEmpty()) { - return - } - - error(failures.joinToString("\n")) - } - - actual fun dumpCoroutines() { - error("Debug probes unsupported native.") - } -} diff --git a/ktor-client/ktor-client-tests/wasmJs/src/io/ktor/client/tests/utils/ClientLoaderWasm.kt b/ktor-client/ktor-client-tests/wasmJs/src/io/ktor/client/tests/utils/ClientLoaderWasm.kt deleted file mode 100644 index f1f7794a390..00000000000 --- a/ktor-client/ktor-client-tests/wasmJs/src/io/ktor/client/tests/utils/ClientLoaderWasm.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package io.ktor.client.tests.utils - -import io.ktor.client.engine.* -import io.ktor.client.engine.js.* -import kotlinx.coroutines.* -import kotlinx.coroutines.test.* - -/** - * Helper interface to test client. - */ -actual abstract class ClientLoader actual constructor(private val timeoutSeconds: Int) { - /** - * Perform test against all clients from dependencies. - */ - actual fun clientTests( - skipEngines: List, - onlyWithEngine: String?, - retries: Int, - block: suspend TestClientBuilder.() -> Unit - ): TestResult { - val skipEnginesLowerCase = skipEngines.map { it.lowercase() } - return if ((onlyWithEngine != null && onlyWithEngine != "js") || skipEnginesLowerCase.contains("js")) { - runTest {} - } else { - testWithEngine(Js, retries = retries, timeoutMillis = timeoutSeconds * 1000L, block = block) - } - } - - actual fun dumpCoroutines() { - error("Debug probes unsupported[wasm]") - } -}