Skip to content

Commit 92025f5

Browse files
authored
KTOR-8043 Add retry to server tests by default (#4593)
1 parent b42d338 commit 92025f5

File tree

12 files changed

+123
-102
lines changed

12 files changed

+123
-102
lines changed

ktor-server/ktor-server-core/jsAndWasmShared/src/io/ktor/server/config/ConfigLoaders.jsAndWasmShared.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/*
2-
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.server.config
66

77
import io.ktor.server.engine.*
8-
import io.ktor.utils.io.*
98

109
internal actual val CONFIG_PATH: List<String>
1110
get() = listOfNotNull(
@@ -18,6 +17,7 @@ internal actual val CONFIG_PATH: List<String>
1817
public actual val configLoaders: List<ConfigLoader>
1918
get() = _configLoaders
2019

20+
@Suppress("ObjectPropertyName")
2121
private val _configLoaders: MutableList<ConfigLoader> = mutableListOf()
2222

2323
public fun addConfigLoader(loader: ConfigLoader) {

ktor-server/ktor-server-test-base/build.gradle.kts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
description = ""
@@ -10,7 +10,7 @@ kotlin.sourceSets {
1010
commonMain {
1111
dependencies {
1212
api(project(":ktor-server:ktor-server-test-host"))
13-
api(libs.kotlin.test)
13+
api(project(":ktor-shared:ktor-test-base"))
1414
}
1515
}
1616

@@ -21,7 +21,6 @@ kotlin.sourceSets {
2121
api(project(":ktor-client:ktor-client-apache"))
2222
api(project(":ktor-network:ktor-network-tls:ktor-network-tls-certificates"))
2323
api(project(":ktor-server:ktor-server-plugins:ktor-server-call-logging"))
24-
api(project(":ktor-shared:ktor-test-base"))
2524

2625
if (jetty_alpn_boot_version != null) {
2726
api(libs.jetty.alpn.boot)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.server.test.base
@@ -16,21 +16,15 @@ expect abstract class BaseTest() {
1616
open fun afterTest()
1717

1818
fun collectUnhandledException(error: Throwable) // TODO: better name?
19-
fun runTest(timeout: Duration = 60.seconds, block: suspend CoroutineScope.() -> Unit): TestResult
19+
fun runTest(
20+
timeout: Duration = 60.seconds,
21+
retries: Int = DEFAULT_RETRIES,
22+
block: suspend CoroutineScope.() -> Unit
23+
): TestResult
2024
}
2125

22-
fun BaseTest.runTest(
23-
retry: Int,
24-
timeout: Duration = this.timeout,
25-
block: suspend CoroutineScope.() -> Unit
26-
): TestResult {
27-
lateinit var lastCause: Throwable
28-
repeat(retry) {
29-
try {
30-
return runTest(timeout, block)
31-
} catch (cause: Throwable) {
32-
lastCause = cause
33-
}
34-
}
35-
throw lastCause
36-
}
26+
/**
27+
* Defaults to `1` on all platforms except for JVM.
28+
* On JVM retries are disabled as we use test-retry Gradle plugin instead.
29+
*/
30+
internal expect val DEFAULT_RETRIES: Int
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package io.ktor.server.test.base
6+
7+
import io.ktor.util.*
8+
import kotlinx.coroutines.test.TestResult
9+
import kotlin.test.Test
10+
import kotlin.test.fail
11+
12+
class BaseTestTest : BaseTest() {
13+
14+
@Test
15+
fun `runTest - retry test by default on non-JVM platform`(): TestResult {
16+
var retryCount = 0
17+
return runTest {
18+
if (!PlatformUtils.IS_JVM && retryCount++ < 1) fail("This test should be retried")
19+
}
20+
}
21+
22+
@Test
23+
fun `runTest - don't retry test by default on JVM platform`(): TestResult {
24+
var retryCount = 0
25+
return runTest {
26+
if (PlatformUtils.IS_JVM && retryCount++ > 0) fail("This test should not be retried")
27+
}
28+
}
29+
30+
@Test
31+
fun `runTest - more than one retry`(): TestResult {
32+
var retryCount = 0
33+
return runTest(retries = 3) {
34+
if (retryCount++ < 3) fail("This test should be retried")
35+
}
36+
}
37+
38+
@Test
39+
fun `runTest - retry should work with collected exceptions`(): TestResult {
40+
var retryCount = 0
41+
return runTest(retries = 1) {
42+
if (retryCount++ < 1) collectUnhandledException(Exception("This test should be retried"))
43+
}
44+
}
45+
}

ktor-server/ktor-server-test-base/jsAndWasmShared/src/io/ktor/server/test/base/BaseTest.jsAndWasmShared.kt

-52
This file was deleted.

ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/BaseTestJvm.kt

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.server.test.base
66

7+
import io.ktor.test.*
78
import io.ktor.test.dispatcher.*
89
import io.ktor.test.junit.*
910
import io.ktor.test.junit.coroutines.*
@@ -47,13 +48,20 @@ actual abstract class BaseTest actual constructor() {
4748

4849
actual fun runTest(
4950
timeout: Duration,
51+
retries: Int,
5052
block: suspend CoroutineScope.() -> Unit
51-
): TestResult = runTestWithRealTime(CoroutineName("test-$testName"), timeout) {
52-
beforeTest()
53-
try {
54-
block()
55-
} finally {
56-
afterTest()
53+
): TestResult = retryTest(retries) { retry ->
54+
runTestWithRealTime(CoroutineName("test-$testName"), timeout) {
55+
if (retry > 0) println("[Retry $retry/$retries]")
56+
beforeTest()
57+
try {
58+
block()
59+
} finally {
60+
afterTest()
61+
}
5762
}
5863
}
5964
}
65+
66+
/** On JVM retries are disabled as we use test-retry Gradle plugin instead. */
67+
internal actual const val DEFAULT_RETRIES: Int = 0

ktor-server/ktor-server-test-base/posix/src/io/ktor/server/test/base/BaseTestNix.kt ktor-server/ktor-server-test-base/nonJvm/src/io/ktor/server/test/base/BaseTest.nonJvm.kt

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.server.test.base
66

7+
import io.ktor.test.*
78
import io.ktor.test.dispatcher.*
89
import io.ktor.utils.io.*
910
import io.ktor.utils.io.locks.*
@@ -12,15 +13,14 @@ import kotlinx.coroutines.test.TestResult
1213
import kotlin.time.Duration
1314
import kotlin.time.Duration.Companion.seconds
1415

16+
@OptIn(InternalAPI::class)
1517
actual abstract class BaseTest actual constructor() {
1618
actual open val timeout: Duration = 10.seconds
1719

1820
private val errors = mutableListOf<Throwable>()
1921

20-
@OptIn(InternalAPI::class)
2122
private val errorsLock = SynchronizedObject()
2223

23-
@OptIn(InternalAPI::class)
2424
actual fun collectUnhandledException(error: Throwable) {
2525
synchronized(errorsLock) {
2626
errors.add(error)
@@ -31,6 +31,9 @@ actual abstract class BaseTest actual constructor() {
3131
}
3232

3333
actual open fun afterTest() {
34+
val errors = synchronized(errorsLock) { errors.toList() }
35+
this.errors.clear()
36+
3437
if (errors.isEmpty()) return
3538

3639
val error = UnhandledErrorsException(
@@ -46,15 +49,21 @@ actual abstract class BaseTest actual constructor() {
4649

4750
actual fun runTest(
4851
timeout: Duration,
52+
retries: Int,
4953
block: suspend CoroutineScope.() -> Unit
50-
): TestResult = runTestWithRealTime(timeout = timeout) {
51-
beforeTest()
52-
try {
53-
block()
54-
} finally {
55-
afterTest()
54+
): TestResult = retryTest(retries) { retry ->
55+
runTestWithRealTime(timeout = timeout) {
56+
if (retry > 0) println("[Retry $retry/$retries]")
57+
beforeTest()
58+
try {
59+
block()
60+
} finally {
61+
afterTest()
62+
}
5663
}
5764
}
5865
}
5966

67+
internal actual const val DEFAULT_RETRIES: Int = 1
68+
6069
private class UnhandledErrorsException(override val message: String) : Exception()

ktor-shared/ktor-test-base/common/src/io/ktor/test/TestResult.kt

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.test
@@ -18,4 +18,21 @@ expect inline fun TestResult.andThen(crossinline block: () -> Any): TestResult
1818
internal expect inline fun testWithRecover(noinline recover: (Throwable) -> Unit, test: () -> TestResult): TestResult
1919

2020
internal expect inline fun <T> runTestForEach(items: Iterable<T>, crossinline test: (T) -> TestResult): TestResult
21-
internal expect inline fun retryTest(retries: Int, crossinline test: (Int) -> TestResult): TestResult
21+
22+
/**
23+
* Executes a test function with retry capabilities.
24+
*
25+
* ```
26+
* retryTest(retires = 2) { retry ->
27+
* runTest {
28+
* println("This test passes only on second retry. Current retry is $retry")
29+
* assertEquals(2, retry)
30+
* }
31+
* }
32+
* ```
33+
*
34+
* @param retries The number of retries to attempt after an initial failure. Must be a non-negative integer.
35+
* @param test A test to execute, which accepts the current retry attempt (starting at 0) as an argument.
36+
* @return A [TestResult] representing the outcome of the test after all attempts.
37+
*/
38+
expect inline fun retryTest(retries: Int, crossinline test: (Int) -> TestResult): TestResult

ktor-shared/ktor-test-base/common/test/io/ktor/test/RunTestWithDataTest.kt

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.test
@@ -101,18 +101,18 @@ class RunTestWithDataTest {
101101
fun testRetriesHaveIndependentTimeout() = runTestWithData(
102102
singleTestCase,
103103
retries = 1,
104-
timeout = 30.milliseconds,
104+
timeout = 50.milliseconds,
105105
test = { (_, retry) ->
106-
realTimeDelay(20.milliseconds)
106+
realTimeDelay(30.milliseconds)
107107
if (retry == 0) fail("Try again, please")
108108
},
109109
)
110110

111111
@Test
112112
fun testDifferentItemsHaveIndependentTimeout() = runTestWithData(
113113
testCases = 1..2,
114-
timeout = 30.milliseconds,
115-
test = { realTimeDelay(20.milliseconds) },
114+
timeout = 50.milliseconds,
115+
test = { realTimeDelay(30.milliseconds) },
116116
)
117117

118118
@Test

ktor-shared/ktor-test-base/jsAndWasmShared/src/io/ktor/test/TestResult.jsAndWasmShared.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.test
@@ -14,7 +14,8 @@ internal actual inline fun testWithRecover(
1414
internal actual inline fun <T> runTestForEach(items: Iterable<T>, crossinline test: (T) -> TestResult): TestResult =
1515
items.fold(DummyTestResult) { acc, item -> acc.andThen { test(item) } }
1616

17-
internal actual inline fun retryTest(retries: Int, crossinline test: (Int) -> TestResult): TestResult =
17+
actual inline fun retryTest(retries: Int, crossinline test: (Int) -> TestResult): TestResult =
1818
(1..retries).fold(test(0)) { acc, retry -> acc.catch { test(retry) } }
1919

20+
@PublishedApi
2021
internal expect fun TestResult.catch(action: (Throwable) -> Any): TestResult

ktor-shared/ktor-test-base/jvm/src/io/ktor/test/junit/ErrorCollector.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.test.junit

ktor-shared/ktor-test-base/jvmAndPosix/src/io/ktor/test/TestResult.jvmAndPosix.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package io.ktor.test
@@ -25,7 +25,7 @@ internal actual inline fun <T> runTestForEach(items: Iterable<T>, test: (T) -> T
2525
return DummyTestResult
2626
}
2727

28-
internal actual inline fun retryTest(retries: Int, test: (Int) -> TestResult): TestResult {
28+
actual inline fun retryTest(retries: Int, test: (Int) -> TestResult): TestResult {
2929
lateinit var lastCause: Throwable
3030
repeat(retries + 1) { attempt ->
3131
try {

0 commit comments

Comments
 (0)