From e810eaabccc5ba8aa144af361c7638053aff01fb Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Tue, 17 Dec 2024 17:59:39 +0100 Subject: [PATCH] Refactor ClientLoader.clientTests (#4548) * Disable mocha timeout * Use Duration for timeout in test utils * Rename 'ktor-junit' -> 'ktor-test-base' * Add multiplatform implementation of runTestWithData * Use runTestWithData in ClientLoader * Decrease timeouts --- build.gradle.kts | 4 +- buildSrc/src/main/kotlin/JsConfig.kt | 17 +- gradle/compatibility.gradle | 2 +- karma/chrome_bin.js | 3 +- ktor-client/ktor-client-cio/build.gradle.kts | 2 +- .../ktor/client/engine/cio/CIORequestTest.kt | 2 +- .../client/engine/cio/ConnectErrorsTest.kt | 2 +- .../ktor-client-tests/build.gradle.kts | 5 +- .../ktor/client/tests/utils/ClientLoader.kt | 61 +++--- .../tests/utils/CommonClientTestUtils.kt | 78 +++----- .../test/io/ktor/client/tests/ContentTest.kt | 2 +- .../tests/plugins/ServerSentEventsTest.kt | 5 +- .../ktor/client/tests/utils/TestWithKtor.kt | 2 +- .../io/ktor/client/tests/LoggingTestJvm.kt | 5 +- ktor-network/build.gradle.kts | 2 +- .../network/sockets/tests/ClientSocketTest.kt | 4 +- .../network/sockets/tests/ServerSocketTest.kt | 2 +- .../ktor-network-tls/build.gradle.kts | 2 +- .../io/ktor/network/tls/ConnectionTest.kt | 2 +- .../ktor-server-metrics/build.gradle.kts | 2 +- .../dropwizard/DropwizardMetricsTests.kt | 10 +- .../ktor-server-websockets/build.gradle.kts | 2 +- .../io/ktor/tests/websocket/WebSocketTest.kt | 2 +- .../ktor-server-test-base/build.gradle.kts | 2 +- .../io/ktor/server/test/base/BaseTestJvm.kt | 4 +- .../server/test/base/EngineTestBaseJvm.kt | 2 +- .../testing/suites/ClientCertTestSuite.kt | 2 +- .../build.gradle.kts | 2 - .../build.gradle.kts | 9 +- .../common/src/io/ktor/test/TestResult.kt | 21 ++ .../src/io/ktor/test/runTestWithData.kt | 114 +++++++++++ .../test/io/ktor/test/RunTestWithDataTest.kt | 183 ++++++++++++++++++ .../js/src/io/ktor/test/TestResult.js.kt | 24 +++ .../ktor/test/TestResult.jsAndWasmShared.kt | 20 ++ .../jvm/src/io/ktor/test}/junit/Assertions.kt | 2 +- .../src/io/ktor/test}/junit/ErrorCollector.kt | 4 +- .../test}/junit/MultipleFailureException.kt | 2 +- .../coroutines/CoroutinesTimeoutExtension.kt | 2 +- .../io/ktor/test/TestResult.jvmAndPosix.kt | 38 ++++ .../src/io/ktor/test/TestResult.wasmJs.kt | 28 +++ ktor-utils/build.gradle.kts | 2 +- .../tests/utils/DeflaterReadChannelTest.kt | 2 +- .../io/ktor/tests/utils/FileChannelTest.kt | 2 +- settings.gradle.kts | 2 +- 44 files changed, 549 insertions(+), 136 deletions(-) rename ktor-shared/{ktor-junit => ktor-test-base}/build.gradle.kts (63%) create mode 100644 ktor-shared/ktor-test-base/common/src/io/ktor/test/TestResult.kt create mode 100644 ktor-shared/ktor-test-base/common/src/io/ktor/test/runTestWithData.kt create mode 100644 ktor-shared/ktor-test-base/common/test/io/ktor/test/RunTestWithDataTest.kt create mode 100644 ktor-shared/ktor-test-base/js/src/io/ktor/test/TestResult.js.kt create mode 100644 ktor-shared/ktor-test-base/jsAndWasmShared/src/io/ktor/test/TestResult.jsAndWasmShared.kt rename ktor-shared/{ktor-junit/jvm/src/io/ktor => ktor-test-base/jvm/src/io/ktor/test}/junit/Assertions.kt (96%) rename ktor-shared/{ktor-junit/jvm/src/io/ktor => ktor-test-base/jvm/src/io/ktor/test}/junit/ErrorCollector.kt (94%) rename ktor-shared/{ktor-junit/jvm/src/io/ktor => ktor-test-base/jvm/src/io/ktor/test}/junit/MultipleFailureException.kt (91%) rename ktor-shared/{ktor-junit/jvm/src/io/ktor => ktor-test-base/jvm/src/io/ktor/test}/junit/coroutines/CoroutinesTimeoutExtension.kt (99%) create mode 100644 ktor-shared/ktor-test-base/jvmAndPosix/src/io/ktor/test/TestResult.jvmAndPosix.kt create mode 100644 ktor-shared/ktor-test-base/wasmJs/src/io/ktor/test/TestResult.wasmJs.kt diff --git a/build.gradle.kts b/build.gradle.kts index 77d02c183aa..0763684337e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,7 +29,7 @@ apply(from = "gradle/verifier.gradle") extra["skipPublish"] = mutableListOf( "ktor-server-test-suites", "ktor-server-tests", - "ktor-junit", + "ktor-test-base", ) // Point old artifact to new location @@ -48,7 +48,7 @@ val disabledExplicitApiModeProjects = listOf( "ktor-server-test-suites", "ktor-server-tests", "ktor-client-content-negotiation-tests", - "ktor-junit" + "ktor-test-base" ) apply(from = "gradle/compatibility.gradle") diff --git a/buildSrc/src/main/kotlin/JsConfig.kt b/buildSrc/src/main/kotlin/JsConfig.kt index 161e05c452e..e7f5dfe772e 100644 --- a/buildSrc/src/main/kotlin/JsConfig.kt +++ b/buildSrc/src/main/kotlin/JsConfig.kt @@ -2,13 +2,11 @@ * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -import internal.* -import org.gradle.api.* -import org.gradle.internal.extensions.stdlib.* -import org.gradle.kotlin.dsl.* -import org.jetbrains.kotlin.gradle.targets.js.dsl.* -import java.io.* -import kotlin.toString +import internal.capitalized +import internal.libs +import org.gradle.api.Project +import org.gradle.kotlin.dsl.invoke +import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsSubTargetDsl fun Project.configureJs() { kotlin { @@ -34,7 +32,8 @@ fun Project.configureJs() { internal fun KotlinJsSubTargetDsl.useMochaForTests() { testTask { useMocha { - timeout = "10000" + // Disable timeout as we use individual timeouts for tests + timeout = "0" } } } @@ -43,7 +42,7 @@ internal fun KotlinJsSubTargetDsl.useKarmaForTests() { testTask { useKarma { useChromeHeadless() - useConfigDirectory(File(project.rootProject.projectDir, "karma")) + useConfigDirectory(project.rootProject.file("karma")) } } } diff --git a/gradle/compatibility.gradle b/gradle/compatibility.gradle index 85162b1c179..8b612485e8a 100644 --- a/gradle/compatibility.gradle +++ b/gradle/compatibility.gradle @@ -12,7 +12,7 @@ apiValidation { 'ktor-client-plugins', 'ktor-server-test-suites', "ktor-server-test-base", - 'ktor-junit', + 'ktor-test-base', ].toSet() def projects = [].toSet() diff --git a/karma/chrome_bin.js b/karma/chrome_bin.js index edde16b07f1..e8b1926e4e3 100644 --- a/karma/chrome_bin.js +++ b/karma/chrome_bin.js @@ -21,7 +21,8 @@ config.set({ "client": { captureConsole: true, "mocha": { - timeout: 10000 + // Disable timeout as we use individual timeouts for tests + timeout: 0 } } }); diff --git a/ktor-client/ktor-client-cio/build.gradle.kts b/ktor-client/ktor-client-cio/build.gradle.kts index 8e338ce5ab1..cc1412aa042 100644 --- a/ktor-client/ktor-client-cio/build.gradle.kts +++ b/ktor-client/ktor-client-cio/build.gradle.kts @@ -24,7 +24,7 @@ kotlin { jvmTest { dependencies { api(project(":ktor-network:ktor-network-tls:ktor-network-tls-certificates")) - api(project(":ktor-shared:ktor-junit")) + api(project(":ktor-shared:ktor-test-base")) implementation(libs.mockk) } } diff --git a/ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/CIORequestTest.kt b/ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/CIORequestTest.kt index 22f7fc2d650..c58d4da5e20 100644 --- a/ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/CIORequestTest.kt +++ b/ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/CIORequestTest.kt @@ -12,11 +12,11 @@ import io.ktor.client.statement.* import io.ktor.client.tests.utils.* import io.ktor.http.* import io.ktor.http.content.* -import io.ktor.junit.coroutines.* import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.ktor.test.junit.coroutines.* import io.mockk.mockkStatic import io.mockk.verify import kotlinx.coroutines.delay diff --git a/ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/ConnectErrorsTest.kt b/ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/ConnectErrorsTest.kt index 80702b6a17e..f7516c2ae24 100644 --- a/ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/ConnectErrorsTest.kt +++ b/ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/ConnectErrorsTest.kt @@ -11,7 +11,7 @@ import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.junit.coroutines.* +import io.ktor.test.junit.coroutines.* import io.ktor.network.tls.certificates.* import io.ktor.server.application.* import io.ktor.server.engine.* diff --git a/ktor-client/ktor-client-tests/build.gradle.kts b/ktor-client/ktor-client-tests/build.gradle.kts index cf6c91fdbd8..c620c040210 100644 --- a/ktor-client/ktor-client-tests/build.gradle.kts +++ b/ktor-client/ktor-client-tests/build.gradle.kts @@ -17,8 +17,7 @@ kotlin.sourceSets { dependencies { api(project(":ktor-client:ktor-client-mock")) api(project(":ktor-test-dispatcher")) - api(libs.kotlin.test) - api(libs.kotlinx.coroutines.test) + api(project(":ktor-shared:ktor-test-base")) } } commonTest { @@ -46,7 +45,7 @@ kotlin.sourceSets { api(project(":ktor-server:ktor-server-plugins:ktor-server-auth")) api(project(":ktor-server:ktor-server-plugins:ktor-server-websockets")) api(project(":ktor-shared:ktor-serialization:ktor-serialization-kotlinx")) - api(project(":ktor-shared:ktor-junit")) + api(project(":ktor-shared:ktor-test-base")) api(libs.logback.classic) implementation(libs.kotlinx.coroutines.debug) } 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 a4464477a44..a7b57c29616 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 @@ -1,12 +1,12 @@ /* - * Copyright 2014-2019 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.utils import io.ktor.client.engine.* +import io.ktor.test.* import kotlinx.coroutines.test.TestResult -import kotlinx.coroutines.test.runTest import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes @@ -15,6 +15,8 @@ internal expect val platformName: String internal expect fun platformDumpCoroutines() internal expect fun platformWaitForAllCoroutines() +private typealias ClientTestFailure = TestFailure> + /** * Helper interface to test client. */ @@ -26,39 +28,38 @@ abstract class ClientLoader(private val timeout: Duration = 1.minutes) { skipEngines: List = emptyList(), onlyWithEngine: String? = null, retries: Int = 1, + timeout: Duration = this.timeout, block: suspend TestClientBuilder.() -> Unit - ): TestResult = runTest(timeout = timeout) { + ): TestResult { 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 - } + val (selectedEngines, skippedEngines) = enginesToTest + .partition { shouldRun(it.engineName, skipPatterns, onlyWithEngine) } + if (skippedEngines.isNotEmpty()) println("Skipped engines: ${skippedEngines.joinToString { it.engineName }}") + + return runTestWithData( + selectedEngines, + timeout = timeout, + retries = retries, + handleFailures = ::aggregatedAssertionError, + ) { (engine, retry) -> + val retrySuffix = if (retry > 0) " [$retry]" else "" + println("Run test with engine ${engine.engineName}$retrySuffix") + performTestWithEngine(engine, this@ClientLoader, block) } + } - 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(" ")) - } + private fun aggregatedAssertionError(failures: List): Nothing { + val message = buildString { + val engineNames = failures.map { it.data.engineName } + if (failures.size > 1) { + appendLine("Test failed for engines: ${engineNames.joinToString()}") + } + failures.forEachIndexed { index, (cause, _) -> + appendLine("Test failed for engine '$platformName:${engineNames[index]}' with:") + appendLine(cause.stackTraceToString().prependIndent(" ")) } - throw AssertionError(message) } + throw AssertionError(message) } private fun shouldRun( @@ -132,5 +133,3 @@ private data class SkipEnginePattern( } } } - -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 25012cd454e..f507e9d3114 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 @@ -6,10 +6,11 @@ package io.ktor.client.tests.utils import io.ktor.client.* import io.ktor.client.engine.* +import io.ktor.test.* import io.ktor.utils.io.core.* import kotlinx.coroutines.* -import kotlinx.coroutines.test.runTest -import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes /** * Web url for tests. @@ -31,29 +32,27 @@ const val TCP_SERVER: String = "http://127.0.0.1:8082" */ fun testWithEngine( engine: HttpClientEngine, - timeoutMillis: Long = 60 * 1000L, + timeout: Duration = 1.minutes, retries: Int = 1, block: suspend TestClientBuilder<*>.() -> Unit -) = testWithClient(HttpClient(engine), timeoutMillis, retries, block) +) = testWithClient(HttpClient(engine), timeout, retries, block) /** * Perform test with selected [client]. */ private fun testWithClient( client: HttpClient, - timeout: Long, + timeout: Duration, retries: Int, block: suspend TestClientBuilder.() -> Unit -) = runTest(timeout = timeout.milliseconds) { +) = runTestWithData(listOf(client), timeout = timeout, retries = retries) { val builder = TestClientBuilder().also { it.block() } - retryTest(retries) { - concurrency(builder.concurrency) { threadId -> - repeat(builder.repeatCount) { attempt -> - @Suppress("UNCHECKED_CAST") - client.config { builder.config(this as HttpClientConfig) } - .use { client -> builder.test(TestInfo(threadId, attempt), client) } - } + concurrency(builder.concurrency) { threadId -> + repeat(builder.repeatCount) { attempt -> + @Suppress("UNCHECKED_CAST") + client.config { builder.config(this as HttpClientConfig) } + .use { client -> builder.test(TestInfo(threadId, attempt), client) } } } @@ -66,18 +65,17 @@ private fun testWithClient( fun testWithEngine( factory: HttpClientEngineFactory, loader: ClientLoader? = null, - timeoutMillis: Long = 60L * 1000L, + timeout: Duration = 1.minutes, retries: Int = 1, block: suspend TestClientBuilder.() -> Unit -) = runTest(timeout = timeoutMillis.milliseconds) { - performTestWithEngine(factory, loader, retries, block) +) = runTestWithData(listOf(factory), timeout = timeout, retries = retries) { + performTestWithEngine(factory, loader, block) } @OptIn(DelicateCoroutinesApi::class) suspend fun performTestWithEngine( factory: HttpClientEngineFactory, loader: ClientLoader? = null, - retries: Int = 1, block: suspend TestClientBuilder.() -> Unit ) { val builder = TestClientBuilder().apply { block() } @@ -89,45 +87,31 @@ suspend fun performTestWithEngine( } } - retryTest(retries) { - withContext(Dispatchers.Default.limitedParallelism(1)) { - concurrency(builder.concurrency) { threadId -> - repeat(builder.repeatCount) { attempt -> - val client = HttpClient(factory, block = builder.config) + withContext(Dispatchers.Default.limitedParallelism(1)) { + concurrency(builder.concurrency) { threadId -> + repeat(builder.repeatCount) { attempt -> + val client = HttpClient(factory, block = builder.config) - client.use { - builder.test(TestInfo(threadId, attempt), it) - } + client.use { + builder.test(TestInfo(threadId, attempt), it) + } - try { - val job = client.coroutineContext[Job]!! - while (job.isActive) { - yield() - } - } catch (cause: Throwable) { - client.cancel("Test failed", cause) - throw cause - } finally { - builder.after(client) + try { + val job = client.coroutineContext[Job]!! + while (job.isActive) { + yield() } + } catch (cause: Throwable) { + client.cancel("Test failed", cause) + throw cause + } finally { + builder.after(client) } } } } } -internal suspend fun retryTest(attempts: Int, block: suspend () -> T): T { - var currentAttempt = 0 - while (true) { - try { - return block() - } catch (cause: Throwable) { - if (currentAttempt >= attempts) throw cause - currentAttempt++ - } - } -} - private suspend fun concurrency(level: Int, block: suspend (Int) -> Unit) { coroutineScope { List(level) { 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 cbdc7f5648d..85b4cd71079 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 @@ -43,7 +43,7 @@ val testArrays = testSize.map { makeArray(it) } -class ContentTest : ClientLoader(timeout = 5.minutes) { +class ContentTest : ClientLoader() { @Test fun testGetFormData() = clientTests { 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 4dd4e669640..903f5f6d609 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 @@ -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-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.plugins @@ -25,9 +25,8 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.test.* -import kotlin.time.Duration.Companion.minutes -class ServerSentEventsTest : ClientLoader(timeout = 2.minutes) { +class ServerSentEventsTest : ClientLoader() { @Test fun testExceptionIfSseIsNotInstalled() = testSuspend { diff --git a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/TestWithKtor.kt b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/TestWithKtor.kt index ee783719c26..e594dd4dd3d 100644 --- a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/TestWithKtor.kt +++ b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/TestWithKtor.kt @@ -6,8 +6,8 @@ package io.ktor.client.tests.utils import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger -import io.ktor.junit.coroutines.* import io.ktor.server.engine.* +import io.ktor.test.junit.coroutines.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.slf4j.LoggerFactory 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 4fdfd102266..1d363b56f8e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 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 @@ -14,7 +14,6 @@ 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>() @@ -28,7 +27,7 @@ private class LoggerWithMdc : Logger { } } -class LoggingTestJvm : ClientLoader(timeout = 1000000.seconds) { +class LoggingTestJvm : ClientLoader() { @OptIn(InternalAPI::class) @Test diff --git a/ktor-network/build.gradle.kts b/ktor-network/build.gradle.kts index a3c871ddec5..df8caca958d 100644 --- a/ktor-network/build.gradle.kts +++ b/ktor-network/build.gradle.kts @@ -24,7 +24,7 @@ kotlin { jvmTest { dependencies { - implementation(project(":ktor-shared:ktor-junit")) + implementation(project(":ktor-shared:ktor-test-base")) implementation(libs.mockk) } } diff --git a/ktor-network/jvm/test/io/ktor/network/sockets/tests/ClientSocketTest.kt b/ktor-network/jvm/test/io/ktor/network/sockets/tests/ClientSocketTest.kt index a5dbb98a91a..231f348c3b0 100644 --- a/ktor-network/jvm/test/io/ktor/network/sockets/tests/ClientSocketTest.kt +++ b/ktor-network/jvm/test/io/ktor/network/sockets/tests/ClientSocketTest.kt @@ -4,10 +4,10 @@ package io.ktor.network.sockets.tests -import io.ktor.junit.* -import io.ktor.junit.coroutines.* import io.ktor.network.selector.* import io.ktor.network.sockets.* +import io.ktor.test.junit.* +import io.ktor.test.junit.coroutines.* import io.ktor.utils.io.* import io.mockk.* import kotlinx.coroutines.asCoroutineDispatcher diff --git a/ktor-network/jvm/test/io/ktor/network/sockets/tests/ServerSocketTest.kt b/ktor-network/jvm/test/io/ktor/network/sockets/tests/ServerSocketTest.kt index f492dcfcf9e..6399ec2be51 100644 --- a/ktor-network/jvm/test/io/ktor/network/sockets/tests/ServerSocketTest.kt +++ b/ktor-network/jvm/test/io/ktor/network/sockets/tests/ServerSocketTest.kt @@ -4,9 +4,9 @@ package io.ktor.network.sockets.tests -import io.ktor.junit.coroutines.* import io.ktor.network.selector.* import io.ktor.network.sockets.* +import io.ktor.test.junit.coroutines.* import io.ktor.utils.io.* import kotlinx.coroutines.* import java.io.IOException diff --git a/ktor-network/ktor-network-tls/build.gradle.kts b/ktor-network/ktor-network-tls/build.gradle.kts index 610ffe0c03e..fac2e734f34 100644 --- a/ktor-network/ktor-network-tls/build.gradle.kts +++ b/ktor-network/ktor-network-tls/build.gradle.kts @@ -12,7 +12,7 @@ kotlin.sourceSets { } jvmTest { dependencies { - api(project(":ktor-shared:ktor-junit")) + api(project(":ktor-shared:ktor-test-base")) api(project(":ktor-network:ktor-network-tls:ktor-network-tls-certificates")) api(libs.netty.handler) api(libs.mockk) diff --git a/ktor-network/ktor-network-tls/jvm/test/io/ktor/network/tls/ConnectionTest.kt b/ktor-network/ktor-network-tls/jvm/test/io/ktor/network/tls/ConnectionTest.kt index 3cccdecd431..d52182c9089 100644 --- a/ktor-network/ktor-network-tls/jvm/test/io/ktor/network/tls/ConnectionTest.kt +++ b/ktor-network/ktor-network-tls/jvm/test/io/ktor/network/tls/ConnectionTest.kt @@ -4,10 +4,10 @@ package io.ktor.network.tls -import io.ktor.junit.coroutines.* import io.ktor.network.selector.* import io.ktor.network.sockets.* import io.ktor.network.tls.certificates.* +import io.ktor.test.junit.coroutines.* import io.ktor.util.cio.* import io.ktor.utils.io.* import io.netty.bootstrap.ServerBootstrap diff --git a/ktor-server/ktor-server-plugins/ktor-server-metrics/build.gradle.kts b/ktor-server/ktor-server-plugins/ktor-server-metrics/build.gradle.kts index 17cb41d9a8d..0354d078e28 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-metrics/build.gradle.kts +++ b/ktor-server/ktor-server-plugins/ktor-server-metrics/build.gradle.kts @@ -11,7 +11,7 @@ kotlin { dependencies { api(project(":ktor-server:ktor-server-plugins:ktor-server-status-pages")) api(project(":ktor-server:ktor-server-plugins:ktor-server-cors")) - api(project(":ktor-shared:ktor-junit")) + api(project(":ktor-shared:ktor-test-base")) } } } diff --git a/ktor-server/ktor-server-plugins/ktor-server-metrics/jvm/test/io/ktor/server/metrics/dropwizard/DropwizardMetricsTests.kt b/ktor-server/ktor-server-plugins/ktor-server-metrics/jvm/test/io/ktor/server/metrics/dropwizard/DropwizardMetricsTests.kt index 0479c7b2fcc..2edbb06777e 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-metrics/jvm/test/io/ktor/server/metrics/dropwizard/DropwizardMetricsTests.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-metrics/jvm/test/io/ktor/server/metrics/dropwizard/DropwizardMetricsTests.kt @@ -4,18 +4,18 @@ package io.ktor.server.metrics.dropwizard -import com.codahale.metrics.* -import com.codahale.metrics.jvm.* +import com.codahale.metrics.MetricRegistry +import com.codahale.metrics.jvm.MemoryUsageGaugeSet import io.ktor.client.request.* import io.ktor.http.* -import io.ktor.junit.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.statuspages.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.testing.* -import org.junit.jupiter.api.Assertions.* -import kotlin.test.* +import io.ktor.test.junit.* +import org.junit.jupiter.api.Assertions.assertEquals +import kotlin.test.Test class DropwizardMetricsTests { diff --git a/ktor-server/ktor-server-plugins/ktor-server-websockets/build.gradle.kts b/ktor-server/ktor-server-plugins/ktor-server-websockets/build.gradle.kts index 54c1967cce4..eae5371b1dd 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-websockets/build.gradle.kts +++ b/ktor-server/ktor-server-plugins/ktor-server-websockets/build.gradle.kts @@ -21,7 +21,7 @@ kotlin.sourceSets { jvmTest { dependencies { - implementation(project(":ktor-shared:ktor-junit")) + implementation(project(":ktor-shared:ktor-test-base")) } } } diff --git a/ktor-server/ktor-server-plugins/ktor-server-websockets/jvm/test/io/ktor/tests/websocket/WebSocketTest.kt b/ktor-server/ktor-server-plugins/ktor-server-websockets/jvm/test/io/ktor/tests/websocket/WebSocketTest.kt index 7b534a26fb7..af69978d4dd 100644 --- a/ktor-server/ktor-server-plugins/ktor-server-websockets/jvm/test/io/ktor/tests/websocket/WebSocketTest.kt +++ b/ktor-server/ktor-server-plugins/ktor-server-websockets/jvm/test/io/ktor/tests/websocket/WebSocketTest.kt @@ -7,12 +7,12 @@ package io.ktor.tests.websocket import io.ktor.client.* import io.ktor.client.plugins.websocket.* import io.ktor.client.plugins.websocket.cio.* -import io.ktor.junit.coroutines.* import io.ktor.serialization.* import io.ktor.server.application.* import io.ktor.server.testing.* import io.ktor.server.websocket.* import io.ktor.server.websocket.WebSockets +import io.ktor.test.junit.coroutines.* import io.ktor.util.* import io.ktor.util.reflect.* import io.ktor.utils.io.charsets.* diff --git a/ktor-server/ktor-server-test-base/build.gradle.kts b/ktor-server/ktor-server-test-base/build.gradle.kts index 1929efe27e9..9f9c3a183f4 100644 --- a/ktor-server/ktor-server-test-base/build.gradle.kts +++ b/ktor-server/ktor-server-test-base/build.gradle.kts @@ -21,7 +21,7 @@ kotlin.sourceSets { api(project(":ktor-client:ktor-client-apache")) api(project(":ktor-network:ktor-network-tls:ktor-network-tls-certificates")) api(project(":ktor-server:ktor-server-plugins:ktor-server-call-logging")) - api(project(":ktor-shared:ktor-junit")) + api(project(":ktor-shared:ktor-test-base")) if (jetty_alpn_boot_version != null) { api(libs.jetty.alpn.boot) diff --git a/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/BaseTestJvm.kt b/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/BaseTestJvm.kt index d85d15afb82..b12de8382f1 100644 --- a/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/BaseTestJvm.kt +++ b/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/BaseTestJvm.kt @@ -4,9 +4,9 @@ package io.ktor.server.test.base -import io.ktor.junit.* -import io.ktor.junit.coroutines.* import io.ktor.test.dispatcher.* +import io.ktor.test.junit.* +import io.ktor.test.junit.coroutines.* import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.TestResult diff --git a/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/EngineTestBaseJvm.kt b/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/EngineTestBaseJvm.kt index 9660a308c25..40d326bd666 100644 --- a/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/EngineTestBaseJvm.kt +++ b/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/EngineTestBaseJvm.kt @@ -11,13 +11,13 @@ import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.junit.* import io.ktor.network.tls.certificates.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.plugins.calllogging.* import io.ktor.server.routing.* import io.ktor.server.testing.* +import io.ktor.test.junit.* import io.ktor.util.* import kotlinx.coroutines.* import org.junit.jupiter.api.* diff --git a/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/ClientCertTestSuite.kt b/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/ClientCertTestSuite.kt index ce53c86efd6..df924725477 100644 --- a/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/ClientCertTestSuite.kt +++ b/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/ClientCertTestSuite.kt @@ -8,12 +8,12 @@ import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* import io.ktor.client.request.* -import io.ktor.junit.coroutines.* import io.ktor.network.tls.* import io.ktor.network.tls.certificates.* import io.ktor.server.engine.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.ktor.test.junit.coroutines.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlin.test.Test diff --git a/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/ktor-serialization-kotlinx-tests/build.gradle.kts b/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/ktor-serialization-kotlinx-tests/build.gradle.kts index 0649a27b549..2d735d2521e 100644 --- a/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/ktor-serialization-kotlinx-tests/build.gradle.kts +++ b/ktor-shared/ktor-serialization/ktor-serialization-kotlinx/ktor-serialization-kotlinx-tests/build.gradle.kts @@ -9,11 +9,9 @@ plugins { kotlin.sourceSets { commonMain { dependencies { - api(libs.kotlin.test) api(kotlin("test-annotations-common")) api(project(":ktor-shared:ktor-serialization:ktor-serialization-kotlinx")) api(project(":ktor-client:ktor-client-tests")) - implementation(libs.kotlinx.coroutines.test) } } jvmMain { diff --git a/ktor-shared/ktor-junit/build.gradle.kts b/ktor-shared/ktor-test-base/build.gradle.kts similarity index 63% rename from ktor-shared/ktor-junit/build.gradle.kts rename to ktor-shared/ktor-test-base/build.gradle.kts index 205a2e2527b..d7e45cbac83 100644 --- a/ktor-shared/ktor-junit/build.gradle.kts +++ b/ktor-shared/ktor-test-base/build.gradle.kts @@ -2,9 +2,16 @@ * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -description = "Common extensions for JUnit 5 testing framework" +description = "Common extensions for testing Ktor" kotlin.sourceSets { + commonMain { + dependencies { + api(libs.kotlin.test) + api(project(":ktor-test-dispatcher")) + } + } + jvmMain { dependencies { api(libs.kotlin.test.junit5) diff --git a/ktor-shared/ktor-test-base/common/src/io/ktor/test/TestResult.kt b/ktor-shared/ktor-test-base/common/src/io/ktor/test/TestResult.kt new file mode 100644 index 00000000000..43406392bd9 --- /dev/null +++ b/ktor-shared/ktor-test-base/common/src/io/ktor/test/TestResult.kt @@ -0,0 +1,21 @@ +/* + * 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.test + +import kotlinx.coroutines.test.TestResult + +internal expect val DummyTestResult: TestResult + +/** + * Executes the provided [block] after the test. + * It is the only way to execute something **after** test on JS/WasmJS targets. + * @see TestResult + */ +expect inline fun TestResult.andThen(crossinline block: () -> Any): TestResult + +internal expect inline fun testWithRecover(noinline recover: (Throwable) -> Unit, test: () -> TestResult): TestResult + +internal expect inline fun runTestForEach(items: Iterable, crossinline test: (T) -> TestResult): TestResult +internal expect inline fun retryTest(retries: Int, crossinline test: (Int) -> TestResult): TestResult diff --git a/ktor-shared/ktor-test-base/common/src/io/ktor/test/runTestWithData.kt b/ktor-shared/ktor-test-base/common/src/io/ktor/test/runTestWithData.kt new file mode 100644 index 00000000000..efd3b8ed3b9 --- /dev/null +++ b/ktor-shared/ktor-test-base/common/src/io/ktor/test/runTestWithData.kt @@ -0,0 +1,114 @@ +/* + * 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.test + +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +/** + * Represents a test case with associated data and retry attempt information. + * + * @property data The input data for the test case. + * @property retry The current retry attempt number for this test case. `0` means the initial test run before retries. + */ +data class TestCase(val data: T, val retry: Int) + +/** + * Represents a failure that occurred during the execution of a test case, along with the associated test data. + * + * @param cause The exception that caused the test to fail. + * @param data The data associated with the test case that failed. + */ +data class TestFailure(val cause: Throwable, val data: T) + +/** + * Executes multiple test cases with retry capabilities and timeout control. + * Timeout is independent for each attempt in each test case. + * + * Example usage: + * ``` + * @Test + * fun dataDrivenTest() = runTestWithData( + * testCases = listOf("test1", "test2"), + * timeout = 10.seconds, + * retries = 2, + * afterEach = { (data, retry), error -> + * println("Test case $data attempt $retry ${if (error != null) "failed" else "succeeded"}") + * }, + * afterAll = { println("All tests completed") }, + * handleFailures = { failures -> + * failures.forEach { (cause, data) -> println("Test $data failed: $cause") } + * }, + * ) { (data, retry) -> + * // test implementation + * } + * ``` + * + * @param testCases Data to be used in tests. Each element represents a separate test case. + * @param context Optional coroutine context for test execution. Defaults to [EmptyCoroutineContext]. + * @param timeout Maximum duration allowed for each test attempt. Defaults to 1 minute. + * @param retries Number of additional attempts after initial failure (`0` means no retries). + * @param afterEach Called after each test case attempt, regardless of success or failure. + * Receives the test case and error (if any occurred). + * @param handleFailures Called after all tests finished if any failures occurred. + * Receives a list of all failed test cases with their last failure cause. + * By default, throws [AssertionError] with an aggregated error message. + * @param afterAll Runs after all tests finished, but before [handleFailures]. + * @param test Test execution block. Receives [TestCase] containing both test data and current retry number. + * + * @return [TestResult] representing the completion of all test cases. + */ +fun runTestWithData( + testCases: Iterable, + context: CoroutineContext = EmptyCoroutineContext, + timeout: Duration = 1.minutes, + retries: Int = 1, + afterEach: (TestCase, Throwable?) -> Unit = { _, _ -> }, + handleFailures: (List>) -> Unit = ::defaultAggregatedError, + afterAll: () -> Unit = {}, + test: suspend TestScope.(TestCase) -> Unit, +): TestResult { + check(retries >= 0) { "Retries count shouldn't be negative but it is $retries" } + + val failures = mutableListOf>() + return runTestForEach(testCases) { data -> + retryTest(retries) { retry -> + val testCase = TestCase(data, retry) + + testWithRecover( + recover = { cause -> + afterEach(testCase, cause) + // Don't rethrow the exception on the last retry, + // save it instead to pass in handleFailures later + if (retry == retries) failures += TestFailure(cause, data) else throw cause + } + ) { + runTest(context, timeout = timeout) { + test(testCase) + afterEach(testCase, null) + } + } + } + }.andThen { + afterAll() + if (failures.isNotEmpty()) handleFailures(failures) + } +} + +private fun defaultAggregatedError(failures: List>): Nothing { + val message = buildString { + appendLine("Test execution failed:") + for ((cause, data) in failures) { + appendLine(" Test case '$data' failed:") + appendLine(cause.stackTraceToString().prependIndent(" ")) + } + } + throw AssertionError(message) +} diff --git a/ktor-shared/ktor-test-base/common/test/io/ktor/test/RunTestWithDataTest.kt b/ktor-shared/ktor-test-base/common/test/io/ktor/test/RunTestWithDataTest.kt new file mode 100644 index 00000000000..0012732b253 --- /dev/null +++ b/ktor-shared/ktor-test-base/common/test/io/ktor/test/RunTestWithDataTest.kt @@ -0,0 +1,183 @@ +/* + * 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.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.test.TestResult +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class RunTestWithDataTest { + + //region Test Cases + @Test + fun testBasicSuccess() = runTestWithData( + singleTestCase, + test = { /* simple successful operation */ }, + ) + + @Test + fun testMultipleTestCases(): TestResult { + val executedItems = mutableSetOf() + return runTestWithData( + testCases = 1..3, + test = { (item, _) -> executedItems.add(item) }, + afterAll = { assertEquals(setOf(1, 2, 3), executedItems) }, + ) + } + + @Test + fun testEmptyTestCases() = runTestWithData( + testCases = emptyList(), + test = { fail("Should not be called") }, + afterEach = { _, _ -> fail("Should not be called") }, + handleFailures = { fail("Should not be called") }, + ) + //endregion + + //region Retries + @Test + fun testMultipleRetriesUntilSuccess(): TestResult { + var successfulRetry = 0 + return runTestWithData( + singleTestCase, + retries = 3, + test = { (_, retry) -> + if (retry < 3) fail("Retry #$retry") + successfulRetry = retry + }, + afterAll = { assertEquals(3, successfulRetry) }, + ) + } + + @Test + fun testZeroRetries() = runTestWithData( + singleTestCase, + retries = 0, + test = { fail("Should fail") }, + handleFailures = { assertEquals(1, it.size) } + ) + + @Test + fun testExhaustedRetries() = runTestWithData( + singleTestCase, + retries = 2, + test = { fail("Always fail") }, + handleFailures = { assertEquals(1, it.size) } + ) + //endregion + + //region Timeout + @Test + fun testFailByTimeout() = runTestWithData( + singleTestCase, + timeout = 10.milliseconds, + test = { realTimeDelay(1.seconds) }, + handleFailures = { assertEquals(1, it.size) }, + ) + + @Test + fun testRetryAfterTimeout(): TestResult { + var successfulRetry = 0 + return runTestWithData( + singleTestCase, + retries = 2, + timeout = 15.milliseconds, + test = { (_, retry) -> + if (retry < 2) realTimeDelay(1.seconds) + successfulRetry = retry + }, + afterAll = { assertEquals(2, successfulRetry) }, + ) + } + + @Test + fun testRetriesHaveIndependentTimeout() = runTestWithData( + singleTestCase, + retries = 1, + timeout = 30.milliseconds, + test = { (_, retry) -> + realTimeDelay(20.milliseconds) + if (retry == 0) fail("Try again, please") + }, + ) + + @Test + fun testDifferentItemsHaveIndependentTimeout() = runTestWithData( + testCases = 1..2, + timeout = 30.milliseconds, + test = { realTimeDelay(20.milliseconds) }, + ) + + @Test + fun testSuccessAfterTimeoutItem(): TestResult { + var successfulItem = 0 + return runTestWithData( + testCases = 1..2, + retries = 0, + timeout = 15.milliseconds, + test = { (item, _) -> + if (item == 1) realTimeDelay(1.seconds) + successfulItem = item + }, + handleFailures = { + assertEquals(1, it.size) + assertEquals(1, it.single().data) + assertEquals(2, successfulItem) + }, + ) + } + //endregion + + @Test + fun testExecutionOrderPreserved(): TestResult { + val executionLog = mutableListOf() + return runTestWithData( + testCases = 1..3, + retries = 1, + timeout = 15.milliseconds, + afterEach = { (id, retry), error -> + val status = if (error == null) "succeeded" else "failed: ${error.message}" + executionLog.add("Test $id: attempt $retry $status") + }, + test = { (id, retry) -> + println("${id}x$retry") + when (id) { + 1 -> if (retry == 0) fail("First attempt failed") + 2 -> if (retry == 0) realTimeDelay(1.seconds) + } + }, + afterAll = { + assertEquals( + listOf( + "Test 1: attempt 0 failed: First attempt failed", + "Test 1: attempt 1 succeeded", + "Test 2: attempt 0 failed: After waiting for 15ms, the test body did not run to completion", + "Test 2: attempt 1 succeeded", + "Test 3: attempt 0 succeeded", + ), + executionLog, + ) + } + ) + } + + @Test + fun testContextPropagation() = runTestWithData( + singleTestCase, + context = CoroutineName("TestContext"), + test = { assertEquals("TestContext", currentCoroutineContext()[CoroutineName]?.name) }, + ) +} + +private val singleTestCase = listOf(1) + +private suspend fun realTimeDelay(duration: Duration) { + withContext(Dispatchers.Default.limitedParallelism(1)) { delay(duration) } +} diff --git a/ktor-shared/ktor-test-base/js/src/io/ktor/test/TestResult.js.kt b/ktor-shared/ktor-test-base/js/src/io/ktor/test/TestResult.js.kt new file mode 100644 index 00000000000..56f11e6ea23 --- /dev/null +++ b/ktor-shared/ktor-test-base/js/src/io/ktor/test/TestResult.js.kt @@ -0,0 +1,24 @@ +/* + * 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.test + +import kotlinx.coroutines.test.TestResult +import kotlin.js.Promise + +internal actual val DummyTestResult: TestResult = Promise.resolve(Unit).asTestResult() + +actual inline fun TestResult.andThen(crossinline block: () -> Any): TestResult = + this.asPromise().then { block() }.asTestResult() + +internal actual fun TestResult.catch(action: (Throwable) -> Any): TestResult = + this.asPromise().catch(action).asTestResult() + +@Suppress("CAST_NEVER_SUCCEEDS") +@PublishedApi +internal fun TestResult.asPromise(): Promise = this as Promise + +@Suppress("CAST_NEVER_SUCCEEDS") +@PublishedApi +internal fun Promise<*>.asTestResult(): TestResult = this as TestResult diff --git a/ktor-shared/ktor-test-base/jsAndWasmShared/src/io/ktor/test/TestResult.jsAndWasmShared.kt b/ktor-shared/ktor-test-base/jsAndWasmShared/src/io/ktor/test/TestResult.jsAndWasmShared.kt new file mode 100644 index 00000000000..2526da9ffad --- /dev/null +++ b/ktor-shared/ktor-test-base/jsAndWasmShared/src/io/ktor/test/TestResult.jsAndWasmShared.kt @@ -0,0 +1,20 @@ +/* + * 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.test + +import kotlinx.coroutines.test.TestResult + +internal actual inline fun testWithRecover( + noinline recover: (Throwable) -> Unit, + test: () -> TestResult +): TestResult = test().catch(recover) + +internal actual inline fun runTestForEach(items: Iterable, crossinline test: (T) -> TestResult): TestResult = + items.fold(DummyTestResult) { acc, item -> acc.andThen { test(item) } } + +internal actual inline fun retryTest(retries: Int, crossinline test: (Int) -> TestResult): TestResult = + (1..retries).fold(test(0)) { acc, retry -> acc.catch { test(retry) } } + +internal expect fun TestResult.catch(action: (Throwable) -> Any): TestResult diff --git a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/Assertions.kt b/ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/Assertions.kt similarity index 96% rename from ktor-shared/ktor-junit/jvm/src/io/ktor/junit/Assertions.kt rename to ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/Assertions.kt index f5e0d07b1e8..77c85e33959 100644 --- a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/Assertions.kt +++ b/ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/Assertions.kt @@ -2,7 +2,7 @@ * Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -package io.ktor.junit +package io.ktor.test.junit /** * Convenience function for asserting on all elements of a collection. diff --git a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/ErrorCollector.kt b/ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/ErrorCollector.kt similarity index 94% rename from ktor-shared/ktor-junit/jvm/src/io/ktor/junit/ErrorCollector.kt rename to ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/ErrorCollector.kt index e83fd31f82e..5654fd71e32 100644 --- a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/ErrorCollector.kt +++ b/ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/ErrorCollector.kt @@ -1,8 +1,8 @@ /* - * Copyright 2014-2023 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.junit +package io.ktor.test.junit import org.junit.jupiter.api.extension.* diff --git a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/MultipleFailureException.kt b/ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/MultipleFailureException.kt similarity index 91% rename from ktor-shared/ktor-junit/jvm/src/io/ktor/junit/MultipleFailureException.kt rename to ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/MultipleFailureException.kt index 809a1f41904..02fcd87c309 100644 --- a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/MultipleFailureException.kt +++ b/ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/MultipleFailureException.kt @@ -2,7 +2,7 @@ * Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -package io.ktor.junit +package io.ktor.test.junit class MultipleFailureException(children: List) : RuntimeException( "Exceptions thrown: ${children.joinToString { it::class.simpleName ?: "" }}" diff --git a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/coroutines/CoroutinesTimeoutExtension.kt b/ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/coroutines/CoroutinesTimeoutExtension.kt similarity index 99% rename from ktor-shared/ktor-junit/jvm/src/io/ktor/junit/coroutines/CoroutinesTimeoutExtension.kt rename to ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/coroutines/CoroutinesTimeoutExtension.kt index 2e3a3c9f3e7..0c2365af6d0 100644 --- a/ktor-shared/ktor-junit/jvm/src/io/ktor/junit/coroutines/CoroutinesTimeoutExtension.kt +++ b/ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/coroutines/CoroutinesTimeoutExtension.kt @@ -5,7 +5,7 @@ @file:Suppress("ktlint") @file:OptIn(ExperimentalCoroutinesApi::class) -package io.ktor.junit.coroutines +package io.ktor.test.junit.coroutines import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.debug.DebugProbes diff --git a/ktor-shared/ktor-test-base/jvmAndPosix/src/io/ktor/test/TestResult.jvmAndPosix.kt b/ktor-shared/ktor-test-base/jvmAndPosix/src/io/ktor/test/TestResult.jvmAndPosix.kt new file mode 100644 index 00000000000..fd053639ade --- /dev/null +++ b/ktor-shared/ktor-test-base/jvmAndPosix/src/io/ktor/test/TestResult.jvmAndPosix.kt @@ -0,0 +1,38 @@ +/* + * 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.test + +import kotlinx.coroutines.test.TestResult + +@Suppress("CAST_NEVER_SUCCEEDS") +internal actual val DummyTestResult = Unit as TestResult + +actual inline fun TestResult.andThen(block: () -> Any): TestResult = also { block() } + +internal actual inline fun testWithRecover(recover: (Throwable) -> Unit, test: () -> TestResult): TestResult { + try { + test() + } catch (cause: Throwable) { + recover(cause) + } + return DummyTestResult +} + +internal actual inline fun runTestForEach(items: Iterable, test: (T) -> TestResult): TestResult { + for (item in items) test(item) + return DummyTestResult +} + +internal actual inline fun retryTest(retries: Int, test: (Int) -> TestResult): TestResult { + lateinit var lastCause: Throwable + repeat(retries + 1) { attempt -> + try { + return test(attempt) + } catch (cause: Throwable) { + lastCause = cause + } + } + throw lastCause +} diff --git a/ktor-shared/ktor-test-base/wasmJs/src/io/ktor/test/TestResult.wasmJs.kt b/ktor-shared/ktor-test-base/wasmJs/src/io/ktor/test/TestResult.wasmJs.kt new file mode 100644 index 00000000000..9bb9a540210 --- /dev/null +++ b/ktor-shared/ktor-test-base/wasmJs/src/io/ktor/test/TestResult.wasmJs.kt @@ -0,0 +1,28 @@ +/* + * 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.test + +import kotlinx.coroutines.test.TestResult +import kotlin.js.Promise + +internal actual val DummyTestResult: TestResult = Promise.resolve(Unit.asJsAny()).asTestResult() + +actual inline fun TestResult.andThen(crossinline block: () -> Any): TestResult = + asPromise().then { block().asJsAny() }.asTestResult() + +internal actual inline fun TestResult.catch(crossinline action: (Throwable) -> Any): TestResult = + asPromise().catch { action(it.toThrowableOrNull()!!).asJsAny() }.asTestResult() + +@Suppress("CAST_NEVER_SUCCEEDS") +@PublishedApi +internal fun TestResult.asPromise(): Promise = this as Promise + +@Suppress("CAST_NEVER_SUCCEEDS") +@PublishedApi +internal fun Promise<*>.asTestResult(): TestResult = this as TestResult + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +@PublishedApi +internal fun Any.asJsAny() = this as JsAny diff --git a/ktor-utils/build.gradle.kts b/ktor-utils/build.gradle.kts index c2388bf751f..b7caba18221 100644 --- a/ktor-utils/build.gradle.kts +++ b/ktor-utils/build.gradle.kts @@ -30,7 +30,7 @@ kotlin { } jvmTest { dependencies { - implementation(project(":ktor-shared:ktor-junit")) + implementation(project(":ktor-shared:ktor-test-base")) } } } diff --git a/ktor-utils/jvm/test/io/ktor/tests/utils/DeflaterReadChannelTest.kt b/ktor-utils/jvm/test/io/ktor/tests/utils/DeflaterReadChannelTest.kt index 742e92f1ddb..2602ef020b9 100644 --- a/ktor-utils/jvm/test/io/ktor/tests/utils/DeflaterReadChannelTest.kt +++ b/ktor-utils/jvm/test/io/ktor/tests/utils/DeflaterReadChannelTest.kt @@ -4,7 +4,7 @@ package io.ktor.tests.utils -import io.ktor.junit.coroutines.* +import io.ktor.test.junit.coroutines.* import io.ktor.util.* import io.ktor.util.cio.* import io.ktor.utils.io.* diff --git a/ktor-utils/jvm/test/io/ktor/tests/utils/FileChannelTest.kt b/ktor-utils/jvm/test/io/ktor/tests/utils/FileChannelTest.kt index bcad848abe1..6f6ca2d821b 100644 --- a/ktor-utils/jvm/test/io/ktor/tests/utils/FileChannelTest.kt +++ b/ktor-utils/jvm/test/io/ktor/tests/utils/FileChannelTest.kt @@ -4,7 +4,7 @@ package io.ktor.tests.utils -import io.ktor.junit.* +import io.ktor.test.junit.* import io.ktor.util.cio.* import io.ktor.utils.io.* import io.ktor.utils.io.jvm.javaio.* diff --git a/settings.gradle.kts b/settings.gradle.kts index eb60c53a47d..8f92f0a6be3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -132,5 +132,5 @@ include(":ktor-shared:ktor-events") include(":ktor-shared:ktor-websocket-serialization") include(":ktor-shared:ktor-websockets") include(":ktor-shared:ktor-sse") -include(":ktor-shared:ktor-junit") +include(":ktor-shared:ktor-test-base") include(":ktor-java-modules-test")