Skip to content

Indicate the use of virtual time in withTimeout in TestScope #3591

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ public final class kotlinx/coroutines/ThreadPoolDispatcherKt {
}

public final class kotlinx/coroutines/TimeoutCancellationException : java/util/concurrent/CancellationException, kotlinx/coroutines/CopyableThrowable {
public fun <init> (Ljava/lang/String;Lkotlinx/coroutines/Job;)V
public synthetic fun createCopy ()Ljava/lang/Throwable;
public fun createCopy ()Lkotlinx/coroutines/TimeoutCancellationException;
}
Expand Down
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/common/src/Timeout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ private class TimeoutCoroutine<U, in T: U>(
/**
* This exception is thrown by [withTimeout] to indicate timeout.
*/
public class TimeoutCancellationException internal constructor(
public class TimeoutCancellationException @PublishedApi internal constructor(
message: String,
@JvmField @Transient internal val coroutine: Job?
) : CancellationException(message), CopyableThrowable<TimeoutCancellationException> {
Expand Down
2 changes: 2 additions & 0 deletions kotlinx-coroutines-test/api/kotlinx-coroutines-test.api
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ public final class kotlinx/coroutines/test/TestScopeKt {
public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestScope;)J
public static final fun getTestTimeSource (Lkotlinx/coroutines/test/TestScope;)Lkotlin/time/TimeSource;
public static final fun runCurrent (Lkotlinx/coroutines/test/TestScope;)V
public static final fun withTimeout (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun withTimeout-dWUq8MI (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor {
Expand Down
92 changes: 92 additions & 0 deletions kotlinx-coroutines-test/common/src/TestScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package kotlinx.coroutines.test

import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import kotlinx.coroutines.selects.*
import kotlinx.coroutines.test.internal.*
import kotlin.contracts.*
import kotlin.coroutines.*
import kotlin.time.*

Expand Down Expand Up @@ -296,3 +298,93 @@ internal class UncaughtExceptionsBeforeTest : IllegalStateException(
*/
@ExperimentalCoroutinesApi
internal class UncompletedCoroutinesError(message: String) : AssertionError(message)

/**
* This is an override of [kotlinx.coroutines.withTimeout] for [TestScope] that appends a note to
* [TimeoutCancellationException] saying that the timeout was affected by the virtual time.
* Below is the documentation for the original function.
*
* Runs a given suspending [block] of code inside a coroutine with a specified [timeout][timeMillis] and throws
* a [TimeoutCancellationException] if the timeout was exceeded.
* If the given [timeMillis] is non-positive, [TimeoutCancellationException] is thrown immediately.
*
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
* the cancellable suspending function inside the block throws a [TimeoutCancellationException].
*
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
*
* **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
* even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some
* resource inside the [block] that needs closing or release outside the block.
* See the
* [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
* section of the coroutines guide for details.
*
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*
* @param timeMillis timeout time in milliseconds.
*/
@OptIn(ExperimentalStdlibApi::class, ExperimentalContracts::class)
public suspend fun <T> TestScope.withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
try {
return kotlinx.coroutines.withTimeout(timeMillis, block)
} catch (e: TimeoutCancellationException) {
// TODO: check that the virtual time is not disabled, when such an option is introduced
if (currentCoroutineContext()[CoroutineDispatcher] is TestDispatcher) {
// TODO: explain the proper solution when we have the option to disable time controls.
val message = "Timed out after $timeMillis ms of _virtual_ (kotlinx.coroutines.test) time. " +
"To use the real time, wrap 'withTimeout' in 'withContext(Dispatchers.Default.limitedParallelism(1))'"
@Suppress("INVISIBLE_MEMBER")
throw TimeoutCancellationException(message, null).also { it.addSuppressed(e) }
}
throw e
}
}

/**
* This is an override of [kotlinx.coroutines.withTimeout] for [TestScope] that appends a note to
* [TimeoutCancellationException] saying that the timeout was affected by the virtual time.
* Below is the documentation for the original function.
*
* Runs a given suspending [block] of code inside a coroutine with the specified [timeout] and throws
* a [TimeoutCancellationException] if the timeout was exceeded.
* If the given [timeout] is non-positive, [TimeoutCancellationException] is thrown immediately.
*
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
* the cancellable suspending function inside the block throws a [TimeoutCancellationException].
*
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
*
* **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time,
* even right before the return from inside the timeout [block]. Keep this in mind if you open or acquire some
* resource inside the [block] that needs closing or release outside the block.
* See the
* [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources]
* section of the coroutines guide for details.
*
* > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
*/
@OptIn(ExperimentalStdlibApi::class, ExperimentalContracts::class)
public suspend fun <T> TestScope.withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
try {
return kotlinx.coroutines.withTimeout(timeout, block)
} catch (e: TimeoutCancellationException) {
// TODO: check that the virtual time is not disabled, when such an option is introduced
if (currentCoroutineContext()[CoroutineDispatcher] is TestDispatcher) {
// TODO: explain the proper solution when we have the option to disable time controls.
val message = "Timed out after $timeout of _virtual_ (kotlinx.coroutines.test) time. " +
"To use the real time, wrap 'withTimeout' in 'withContext(Dispatchers.Default.limitedParallelism(1))'"
@Suppress("INVISIBLE_MEMBER")
throw TimeoutCancellationException(message, null).also { it.addSuppressed(e) }
}
throw e
}
}
14 changes: 14 additions & 0 deletions kotlinx-coroutines-test/common/test/TestScopeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,20 @@ class TestScopeTest {
}
}

/**
* Tests that [TestScope.withTimeout] notifies the programmer about using the virtual time.
*/
@Test
fun testTimingOutWithVirtualTimeMessage() = runTest {
try {
withTimeout(1_000_000) {
Channel<Unit>().receive()
}
} catch (e: TimeoutCancellationException) {
assertContains(e.message!!, "virtual")
}
}

companion object {
internal val invalidContexts = listOf(
Dispatchers.Default, // not a [TestDispatcher]
Expand Down