aboutsummaryrefslogtreecommitdiffstats
path: root/kotlinx-coroutines-debug
diff options
context:
space:
mode:
Diffstat (limited to 'kotlinx-coroutines-debug')
-rw-r--r--kotlinx-coroutines-debug/README.md2
-rw-r--r--kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api5
-rw-r--r--kotlinx-coroutines-debug/build.gradle17
-rw-r--r--kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt12
-rw-r--r--kotlinx-coroutines-debug/src/DebugProbes.kt7
-rw-r--r--kotlinx-coroutines-debug/src/internal/Attach.kt2
-rw-r--r--kotlinx-coroutines-debug/src/junit/CoroutinesTimeoutImpl.kt81
-rw-r--r--kotlinx-coroutines-debug/src/junit/junit4/CoroutinesTimeout.kt (renamed from kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt)0
-rw-r--r--kotlinx-coroutines-debug/src/junit/junit4/CoroutinesTimeoutStatement.kt30
-rw-r--r--kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeout.kt63
-rw-r--r--kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeoutExtension.kt279
-rw-r--r--kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt87
-rw-r--r--kotlinx-coroutines-debug/test/DebugProbesTest.kt3
-rw-r--r--kotlinx-coroutines-debug/test/SanitizedProbesTest.kt1
-rw-r--r--kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutExtensionTest.kt121
-rw-r--r--kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutInheritanceTest.kt60
-rw-r--r--kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutMethodTest.kt44
-rw-r--r--kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutNestedTest.kt29
-rw-r--r--kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutSimpleTest.kt61
-rw-r--r--kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutTest.kt170
-rw-r--r--kotlinx-coroutines-debug/test/junit5/RegisterExtensionExample.kt20
21 files changed, 985 insertions, 109 deletions
diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md
index f048040b..f7b86022 100644
--- a/kotlinx-coroutines-debug/README.md
+++ b/kotlinx-coroutines-debug/README.md
@@ -61,7 +61,7 @@ stacktraces will be dumped to the console.
### Using as JVM agent
Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup.
-You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.3.jar`.
+You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.5.1.jar`.
Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines.
When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control
[DebugProbes.enableCreationStackTraces] along with agent startup.
diff --git a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api
index b6056c41..5bf70626 100644
--- a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api
+++ b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api
@@ -61,3 +61,8 @@ public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion {
public static synthetic fun seconds$default (Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;JZZILjava/lang/Object;)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
}
+public abstract interface annotation class kotlinx/coroutines/debug/junit5/CoroutinesTimeout : java/lang/annotation/Annotation {
+ public abstract fun cancelOnTimeout ()Z
+ public abstract fun testTimeoutMs ()J
+}
+
diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle
index 46f894d1..43d94d18 100644
--- a/kotlinx-coroutines-debug/build.gradle
+++ b/kotlinx-coroutines-debug/build.gradle
@@ -20,22 +20,21 @@ configurations {
dependencies {
compileOnly "junit:junit:$junit_version"
+ compileOnly "org.junit.jupiter:junit-jupiter-api:$junit5_version"
+ testCompile "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
+ testCompile "org.junit.platform:junit-platform-testkit:1.7.0"
shadowDeps "net.bytebuddy:byte-buddy:$byte_buddy_version"
shadowDeps "net.bytebuddy:byte-buddy-agent:$byte_buddy_version"
compileOnly "io.projectreactor.tools:blockhound:$blockhound_version"
- testCompile "io.projectreactor.tools:blockhound:$blockhound_version"
+ testImplementation "io.projectreactor.tools:blockhound:$blockhound_version"
api "net.java.dev.jna:jna:$jna_version"
api "net.java.dev.jna:jna-platform:$jna_version"
}
-// TODO: JVM IR generates different stacktrace so temporary disable stacktrace tests
-if (rootProject.ext.jvm_ir_enabled) {
- tasks.named('test', Test) {
- filter {
-// excludeTest('kotlinx.coroutines.debug.CoroutinesDumpTest', 'testCreationStackTrace')
- excludeTestsMatching('kotlinx.coroutines.debug.DebugProbesTest')
- }
- }
+java {
+ /* This is needed to be able to run JUnit5 tests. Otherwise, Gradle complains that it can't find the
+ JVM1.6-compatible version of the `junit-jupiter-api` artifact. */
+ disableAutoTargetJvm()
}
jar {
diff --git a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
index 7dd7d58d..190476c4 100644
--- a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
+++ b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt
@@ -18,6 +18,7 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
allowBlockingWhenEnqueuingTasks()
allowServiceLoaderInvocationsOnInit()
allowBlockingCallsInReflectionImpl()
+ allowBlockingCallsInDebugProbes()
/* The predicates that define that BlockHound should only report blocking calls from threads that are part of
the coroutine thread pool and currently execute a CPU-bound coroutine computation. */
addDynamicThreadPredicate { isSchedulerWorker(it) }
@@ -49,6 +50,17 @@ public class CoroutinesBlockHoundIntegration : BlockHoundIntegration {
}
/**
+ * Allow blocking calls inside [kotlinx.coroutines.debug.internal.DebugProbesImpl].
+ */
+ private fun BlockHound.Builder.allowBlockingCallsInDebugProbes() {
+ for (method in listOf("install", "uninstall", "hierarchyToString", "dumpCoroutinesInfo", "dumpDebuggerInfo",
+ "dumpCoroutinesSynchronized", "updateRunningState", "updateState"))
+ {
+ allowBlockingCallsInside("kotlinx.coroutines.debug.internal.DebugProbesImpl", method)
+ }
+ }
+
+ /**
* Allows blocking inside [kotlinx.coroutines.internal.ThreadSafeHeap].
*/
private fun BlockHound.Builder.allowBlockingCallsInThreadSafeHeap() {
diff --git a/kotlinx-coroutines-debug/src/DebugProbes.kt b/kotlinx-coroutines-debug/src/DebugProbes.kt
index 373864ad..ed346d81 100644
--- a/kotlinx-coroutines-debug/src/DebugProbes.kt
+++ b/kotlinx-coroutines-debug/src/DebugProbes.kt
@@ -143,10 +143,3 @@ public object DebugProbes {
*/
public fun dumpCoroutines(out: PrintStream = System.out): Unit = DebugProbesImpl.dumpCoroutines(out)
}
-
-// Stubs which are injected as coroutine probes. Require direct match of signatures
-internal fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame)
-
-internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame)
-internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> =
- DebugProbesImpl.probeCoroutineCreated(completion)
diff --git a/kotlinx-coroutines-debug/src/internal/Attach.kt b/kotlinx-coroutines-debug/src/internal/Attach.kt
index f38447f7..f1cc96e6 100644
--- a/kotlinx-coroutines-debug/src/internal/Attach.kt
+++ b/kotlinx-coroutines-debug/src/internal/Attach.kt
@@ -20,7 +20,7 @@ internal class ByteBuddyDynamicAttach : Function1<Boolean, Unit> {
private fun attach() {
ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE)
val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt")
- val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt")
+ val cl2 = Class.forName("kotlinx.coroutines.debug.internal.DebugProbesKt")
ByteBuddy()
.redefine(cl2)
diff --git a/kotlinx-coroutines-debug/src/junit/CoroutinesTimeoutImpl.kt b/kotlinx-coroutines-debug/src/junit/CoroutinesTimeoutImpl.kt
new file mode 100644
index 00000000..06a84a5b
--- /dev/null
+++ b/kotlinx-coroutines-debug/src/junit/CoroutinesTimeoutImpl.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug
+
+import java.util.concurrent.*
+
+/**
+ * Run [invocation] in a separate thread with the given timeout in ms, after which the coroutines info is dumped and, if
+ * [cancelOnTimeout] is set, the execution is interrupted.
+ *
+ * Assumes that [DebugProbes] are installed. Does not deinstall them.
+ */
+internal inline fun <T : Any?> runWithTimeoutDumpingCoroutines(
+ methodName: String,
+ testTimeoutMs: Long,
+ cancelOnTimeout: Boolean,
+ initCancellationException: () -> Throwable,
+ crossinline invocation: () -> T
+): T {
+ val testStartedLatch = CountDownLatch(1)
+ val testResult = FutureTask {
+ testStartedLatch.countDown()
+ invocation()
+ }
+ /*
+ * We are using hand-rolled thread instead of single thread executor
+ * in order to be able to safely interrupt thread in the end of a test
+ */
+ val testThread = Thread(testResult, "Timeout test thread").apply { isDaemon = true }
+ try {
+ testThread.start()
+ // Await until test is started to take only test execution time into account
+ testStartedLatch.await()
+ return testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS)
+ } catch (e: TimeoutException) {
+ handleTimeout(testThread, methodName, testTimeoutMs, cancelOnTimeout, initCancellationException())
+ } catch (e: ExecutionException) {
+ throw e.cause ?: e
+ }
+}
+
+private fun handleTimeout(testThread: Thread, methodName: String, testTimeoutMs: Long, cancelOnTimeout: Boolean,
+ cancellationException: Throwable): Nothing {
+ val units =
+ if (testTimeoutMs % 1000 == 0L)
+ "${testTimeoutMs / 1000} seconds"
+ else "$testTimeoutMs milliseconds"
+
+ System.err.println("\nTest $methodName timed out after $units\n")
+ System.err.flush()
+
+ DebugProbes.dumpCoroutines()
+ System.out.flush() // Synchronize serr/sout
+
+ /*
+ * Order is important:
+ * 1) Create exception with a stacktrace of hang test
+ * 2) Cancel all coroutines via debug agent API (changing system state!)
+ * 3) Throw created exception
+ */
+ cancellationException.attachStacktraceFrom(testThread)
+ testThread.interrupt()
+ cancelIfNecessary(cancelOnTimeout)
+ // If timed out test throws an exception, we can't do much except ignoring it
+ throw cancellationException
+}
+
+private fun cancelIfNecessary(cancelOnTimeout: Boolean) {
+ if (cancelOnTimeout) {
+ DebugProbes.dumpCoroutinesInfo().forEach {
+ it.job?.cancel()
+ }
+ }
+}
+
+private fun Throwable.attachStacktraceFrom(thread: Thread) {
+ val stackTrace = thread.stackTrace
+ this.stackTrace = stackTrace
+}
diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt b/kotlinx-coroutines-debug/src/junit/junit4/CoroutinesTimeout.kt
index 12bc9475..12bc9475 100644
--- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt
+++ b/kotlinx-coroutines-debug/src/junit/junit4/CoroutinesTimeout.kt
diff --git a/kotlinx-coroutines-debug/src/junit/junit4/CoroutinesTimeoutStatement.kt b/kotlinx-coroutines-debug/src/junit/junit4/CoroutinesTimeoutStatement.kt
new file mode 100644
index 00000000..aa6b8df2
--- /dev/null
+++ b/kotlinx-coroutines-debug/src/junit/junit4/CoroutinesTimeoutStatement.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit4
+
+import kotlinx.coroutines.debug.*
+import org.junit.runner.*
+import org.junit.runners.model.*
+import java.util.concurrent.*
+
+internal class CoroutinesTimeoutStatement(
+ private val testStatement: Statement,
+ private val testDescription: Description,
+ private val testTimeoutMs: Long,
+ private val cancelOnTimeout: Boolean = false
+) : Statement() {
+
+ override fun evaluate() {
+ try {
+ runWithTimeoutDumpingCoroutines(testDescription.methodName, testTimeoutMs, cancelOnTimeout,
+ { TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS) })
+ {
+ testStatement.evaluate()
+ }
+ } finally {
+ DebugProbes.uninstall()
+ }
+ }
+}
diff --git a/kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeout.kt b/kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeout.kt
new file mode 100644
index 00000000..9a8263fe
--- /dev/null
+++ b/kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeout.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+import kotlinx.coroutines.debug.*
+import org.junit.jupiter.api.*
+import org.junit.jupiter.api.extension.*
+import org.junit.jupiter.api.parallel.*
+import java.lang.annotation.*
+
+/**
+ * Coroutines timeout annotation that is similar to JUnit5's [Timeout] annotation. It allows running test methods in a
+ * separate thread, failing them after the provided time limit and interrupting the thread.
+ *
+ * Additionally, it installs [DebugProbes] and dumps all coroutines at the moment of the timeout. It also cancels
+ * coroutines on timeout if [cancelOnTimeout] set to `true`. The dump contains the coroutine creation stack traces.
+ *
+ * This annotation has an effect on test, test factory, test template, and lifecycle methods and test classes that are
+ * annotated with it.
+ *
+ * Annotating a class is the same as annotating every test, test factory, and test template method (but not lifecycle
+ * methods) of that class and its inner test classes, unless any of them is annotated with [CoroutinesTimeout], in which
+ * case their annotation overrides the one on the containing class.
+ *
+ * Declaring [CoroutinesTimeout] on a test factory checks that it finishes in the specified time, but does not check
+ * whether the methods that it produces obey the timeout as well.
+ *
+ * Example usage:
+ * ```
+ * @CoroutinesTimeout(100)
+ * class CoroutinesTimeoutSimpleTest {
+ * // does not time out, as the annotation on the method overrides the class-level one
+ * @CoroutinesTimeout(1000)
+ * @Test
+ * fun classTimeoutIsOverridden() {
+ * runBlocking {
+ * delay(150)
+ * }
+ * }
+ *
+ * // times out in 100 ms, timeout value is taken from the class-level annotation
+ * @Test
+ * fun classTimeoutIsUsed() {
+ * runBlocking {
+ * delay(150)
+ * }
+ * }
+ * }
+ * ```
+ *
+ * @see Timeout
+ */
+@ExtendWith(CoroutinesTimeoutExtension::class)
+@Inherited
+@MustBeDocumented
+@ResourceLock("coroutines timeout", mode = ResourceAccessMode.READ)
+@Retention(value = AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+public annotation class CoroutinesTimeout(
+ val testTimeoutMs: Long,
+ val cancelOnTimeout: Boolean = false
+)
diff --git a/kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeoutExtension.kt b/kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeoutExtension.kt
new file mode 100644
index 00000000..442fdf8c
--- /dev/null
+++ b/kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeoutExtension.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+
+import kotlinx.coroutines.debug.*
+import kotlinx.coroutines.debug.runWithTimeoutDumpingCoroutines
+import org.junit.jupiter.api.extension.*
+import org.junit.platform.commons.support.AnnotationSupport
+import java.lang.reflect.*
+import java.util.*
+import java.util.concurrent.atomic.*
+
+internal class CoroutinesTimeoutException(val timeoutMs: Long): Exception("test timed out after $timeoutMs ms")
+
+/**
+ * This JUnit5 extension allows running test, test factory, test template, and lifecycle methods in a separate thread,
+ * failing them after the provided time limit and interrupting the thread.
+ *
+ * Additionally, it installs [DebugProbes] and dumps all coroutines at the moment of the timeout. It also cancels
+ * coroutines on timeout if [cancelOnTimeout] set to `true`.
+ * [enableCoroutineCreationStackTraces] controls the corresponding [DebugProbes.enableCreationStackTraces] property
+ * and can be optionally disabled to speed-up tests if creation stack traces are not needed.
+ *
+ * Beware that if several tests that use this extension set [enableCoroutineCreationStackTraces] to different values and
+ * execute in parallel, the behavior is ill-defined. In order to avoid conflicts between different instances of this
+ * extension when using JUnit5 in parallel, use [ResourceLock] with resource name `coroutines timeout` on tests that use
+ * it. Note that the tests annotated with [CoroutinesTimeout] already use this [ResourceLock], so there is no need to
+ * annotate them additionally.
+ *
+ * Note that while calls to test factories are verified to finish in the specified time, but the methods that they
+ * produce are not affected by this extension.
+ *
+ * Beware that registering the extension via [CoroutinesTimeout] annotation conflicts with manually registering it on
+ * the same tests via other methods (most notably, [RegisterExtension]) and is prohibited.
+ *
+ * Example of usage:
+ * ```
+ * class HangingTest {
+ * @JvmField
+ * @RegisterExtension
+ * val timeout = CoroutinesTimeoutExtension.seconds(5)
+ *
+ * @Test
+ * fun testThatHangs() = runBlocking {
+ * ...
+ * delay(Long.MAX_VALUE) // somewhere deep in the stack
+ * ...
+ * }
+ * }
+ * ```
+ *
+ * @see [CoroutinesTimeout]
+ * */
+// NB: the constructor is not private so that JUnit is able to call it via reflection.
+internal class CoroutinesTimeoutExtension internal constructor(
+ private val enableCoroutineCreationStackTraces: Boolean = true,
+ private val timeoutMs: Long? = null,
+ private val cancelOnTimeout: Boolean? = null): InvocationInterceptor
+{
+ /**
+ * Creates the [CoroutinesTimeoutExtension] extension with the given timeout in milliseconds.
+ */
+ public constructor(timeoutMs: Long, cancelOnTimeout: Boolean = false,
+ enableCoroutineCreationStackTraces: Boolean = true):
+ this(enableCoroutineCreationStackTraces, timeoutMs, cancelOnTimeout)
+
+ public companion object {
+ /**
+ * Creates the [CoroutinesTimeoutExtension] extension with the given timeout in seconds.
+ */
+ @JvmOverloads
+ public fun seconds(timeout: Int, cancelOnTimeout: Boolean = false,
+ enableCoroutineCreationStackTraces: Boolean = true): CoroutinesTimeoutExtension =
+ CoroutinesTimeoutExtension(enableCoroutineCreationStackTraces, timeout.toLong() * 1000, cancelOnTimeout)
+ }
+
+ /** @see [initialize] */
+ private val debugProbesOwnershipPassed = AtomicBoolean(false)
+
+ private fun tryPassDebugProbesOwnership() = debugProbesOwnershipPassed.compareAndSet(false, true)
+
+ /* We install the debug probes early so that the coroutines launched from the test constructor are captured as well.
+ However, this is not enough as the same extension instance may be reused several times, even cleaning up its
+ resources from the store. */
+ init {
+ DebugProbes.enableCreationStackTraces = enableCoroutineCreationStackTraces
+ DebugProbes.install()
+ }
+
+ // This is needed so that a class with no tests still successfully passes the ownership of DebugProbes to JUnit5.
+ override fun <T : Any?> interceptTestClassConstructor(
+ invocation: InvocationInterceptor.Invocation<T>,
+ invocationContext: ReflectiveInvocationContext<Constructor<T>>,
+ extensionContext: ExtensionContext
+ ): T {
+ initialize(extensionContext)
+ return invocation.proceed()
+ }
+
+ /**
+ * Initialize this extension instance and/or the extension value store.
+ *
+ * It seems that the only way to reliably have JUnit5 clean up after its extensions is to put an instance of
+ * [ExtensionContext.Store.CloseableResource] into the value store corresponding to the extension instance, which
+ * means that [DebugProbes.uninstall] must be placed into the value store. [debugProbesOwnershipPassed] is `true`
+ * if the call to [DebugProbes.install] performed in the constructor of the extension instance was matched with a
+ * placing of [DebugProbes.uninstall] into the value store. We call the process of placing the cleanup procedure
+ * "passing the ownership", as now JUnit5 (and not our code) has to worry about uninstalling the debug probes.
+ *
+ * However, extension instances can be reused with different value stores, and value stores can be reused across
+ * extension instances. This leads to a tricky scheme of performing [DebugProbes.uninstall]:
+ *
+ * * If neither the ownership of this instance's [DebugProbes] was yet passed nor there is any cleanup procedure
+ * stored, it means that we can just store our cleanup procedure, passing the ownership.
+ * * If the ownership was not yet passed, but a cleanup procedure is already stored, we can't just replace it with
+ * another one, as this would lead to imbalance between [DebugProbes.install] and [DebugProbes.uninstall].
+ * Instead, we know that this extension context will at least outlive this use of this instance, so some debug
+ * probes other than the ones from our constructor are already installed and won't be uninstalled during our
+ * operation. We simply uninstall the debug probes that were installed in our constructor.
+ * * If the ownership was passed, but the store is empty, it means that this test instance is reused and, possibly,
+ * the debug probes installed in its constructor were already uninstalled. This means that we have to install them
+ * anew and store an uninstaller.
+ */
+ private fun initialize(extensionContext: ExtensionContext) {
+ val store: ExtensionContext.Store = extensionContext.getStore(
+ ExtensionContext.Namespace.create(CoroutinesTimeoutExtension::class, extensionContext.uniqueId))
+ /** It seems that the JUnit5 documentation does not specify the relationship between the extension instances and
+ * the corresponding [ExtensionContext] (in which the value stores are managed), so it is unclear whether it's
+ * theoretically possible for two extension instances that run concurrently to share an extension context. So,
+ * just in case this risk exists, we synchronize here. */
+ synchronized(store) {
+ if (store["debugProbes"] == null) {
+ if (!tryPassDebugProbesOwnership()) {
+ /** This means that the [DebugProbes.install] call from the constructor of this extensions has
+ * already been matched with a corresponding cleanup procedure for JUnit5, but then JUnit5 cleaned
+ * everything up and later reused the same extension instance for other tests. Therefore, we need to
+ * install the [DebugProbes] anew. */
+ DebugProbes.enableCreationStackTraces = enableCoroutineCreationStackTraces
+ DebugProbes.install()
+ }
+ /** put a fake resource into this extensions's store so that JUnit cleans it up, uninstalling the
+ * [DebugProbes] after this extension instance is no longer needed. **/
+ store.put("debugProbes", ExtensionContext.Store.CloseableResource { DebugProbes.uninstall() })
+ } else if (!debugProbesOwnershipPassed.get()) {
+ /** This instance shares its store with other ones. Because of this, there was no need to install
+ * [DebugProbes], they are already installed, and this fact will outlive this use of this instance of
+ * the extension. */
+ if (tryPassDebugProbesOwnership()) {
+ // We successfully marked the ownership as passed and now may uninstall the extraneous debug probes.
+ DebugProbes.uninstall()
+ }
+ }
+ }
+ }
+
+ override fun interceptTestMethod(
+ invocation: InvocationInterceptor.Invocation<Void>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ) {
+ interceptNormalMethod(invocation, invocationContext, extensionContext)
+ }
+
+ override fun interceptAfterAllMethod(
+ invocation: InvocationInterceptor.Invocation<Void>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ) {
+ interceptLifecycleMethod(invocation, invocationContext, extensionContext)
+ }
+
+ override fun interceptAfterEachMethod(
+ invocation: InvocationInterceptor.Invocation<Void>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ) {
+ interceptLifecycleMethod(invocation, invocationContext, extensionContext)
+ }
+
+ override fun interceptBeforeAllMethod(
+ invocation: InvocationInterceptor.Invocation<Void>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ) {
+ interceptLifecycleMethod(invocation, invocationContext, extensionContext)
+ }
+
+ override fun interceptBeforeEachMethod(
+ invocation: InvocationInterceptor.Invocation<Void>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ) {
+ interceptLifecycleMethod(invocation, invocationContext, extensionContext)
+ }
+
+ override fun <T : Any?> interceptTestFactoryMethod(
+ invocation: InvocationInterceptor.Invocation<T>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ): T = interceptNormalMethod(invocation, invocationContext, extensionContext)
+
+ override fun interceptTestTemplateMethod(
+ invocation: InvocationInterceptor.Invocation<Void>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ) {
+ interceptNormalMethod(invocation, invocationContext, extensionContext)
+ }
+
+ private fun<T> Class<T>.coroutinesTimeoutAnnotation(): Optional<CoroutinesTimeout> =
+ AnnotationSupport.findAnnotation(this, CoroutinesTimeout::class.java).or {
+ enclosingClass?.coroutinesTimeoutAnnotation() ?: Optional.empty()
+ }
+
+ private fun <T: Any?> interceptMethod(
+ useClassAnnotation: Boolean,
+ invocation: InvocationInterceptor.Invocation<T>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ): T {
+ initialize(extensionContext)
+ val testAnnotationOptional =
+ AnnotationSupport.findAnnotation(invocationContext.executable, CoroutinesTimeout::class.java)
+ val classAnnotationOptional = extensionContext.testClass.flatMap { it.coroutinesTimeoutAnnotation() }
+ if (timeoutMs != null && cancelOnTimeout != null) {
+ // this means we @RegisterExtension was used in order to register this extension.
+ if (testAnnotationOptional.isPresent || classAnnotationOptional.isPresent) {
+ /* Using annotations creates a separate instance of the extension, which composes in a strange way: both
+ timeouts are applied. This is at odds with the concept that method-level annotations override the outer
+ rules and may lead to unexpected outcomes, so we prohibit this. */
+ throw UnsupportedOperationException("Using CoroutinesTimeout along with instance field-registered CoroutinesTimeout is prohibited; please use either @RegisterExtension or @CoroutinesTimeout, but not both")
+ }
+ return interceptInvocation(invocation, invocationContext.executable.name, timeoutMs, cancelOnTimeout)
+ }
+ /* The extension was registered via an annotation; check that we succeeded in finding the annotation that led to
+ the extension being registered and taking its parameters. */
+ if (testAnnotationOptional.isEmpty && classAnnotationOptional.isEmpty) {
+ throw UnsupportedOperationException("Timeout was registered with a CoroutinesTimeout annotation, but we were unable to find it. Please report this.")
+ }
+ return when {
+ testAnnotationOptional.isPresent -> {
+ val annotation = testAnnotationOptional.get()
+ interceptInvocation(invocation, invocationContext.executable.name, annotation.testTimeoutMs,
+ annotation.cancelOnTimeout)
+ }
+ useClassAnnotation && classAnnotationOptional.isPresent -> {
+ val annotation = classAnnotationOptional.get()
+ interceptInvocation(invocation, invocationContext.executable.name, annotation.testTimeoutMs,
+ annotation.cancelOnTimeout)
+ }
+ else -> {
+ invocation.proceed()
+ }
+ }
+ }
+
+ private fun<T> interceptNormalMethod(
+ invocation: InvocationInterceptor.Invocation<T>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ): T = interceptMethod(true, invocation, invocationContext, extensionContext)
+
+ private fun interceptLifecycleMethod(
+ invocation: InvocationInterceptor.Invocation<Void>,
+ invocationContext: ReflectiveInvocationContext<Method>,
+ extensionContext: ExtensionContext
+ ) = interceptMethod(false, invocation, invocationContext, extensionContext)
+
+ private fun <T : Any?> interceptInvocation(
+ invocation: InvocationInterceptor.Invocation<T>,
+ methodName: String,
+ testTimeoutMs: Long,
+ cancelOnTimeout: Boolean
+ ): T =
+ runWithTimeoutDumpingCoroutines(methodName, testTimeoutMs, cancelOnTimeout,
+ { CoroutinesTimeoutException(testTimeoutMs) }, { invocation.proceed() })
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt
deleted file mode 100644
index 4baf409d..00000000
--- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.coroutines.debug.junit4
-
-import kotlinx.coroutines.debug.*
-import org.junit.runner.*
-import org.junit.runners.model.*
-import java.util.concurrent.*
-
-internal class CoroutinesTimeoutStatement(
- testStatement: Statement,
- private val testDescription: Description,
- private val testTimeoutMs: Long,
- private val cancelOnTimeout: Boolean = false
-) : Statement() {
-
- private val testStartedLatch = CountDownLatch(1)
-
- private val testResult = FutureTask<Unit> {
- testStartedLatch.countDown()
- testStatement.evaluate()
- }
-
- /*
- * We are using hand-rolled thread instead of single thread executor
- * in order to be able to safely interrupt thread in the end of a test
- */
- private val testThread = Thread(testResult, "Timeout test thread").apply { isDaemon = true }
-
- override fun evaluate() {
- try {
- testThread.start()
- // Await until test is started to take only test execution time into account
- testStartedLatch.await()
- testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS)
- return
- } catch (e: TimeoutException) {
- handleTimeout(testDescription)
- } catch (e: ExecutionException) {
- throw e.cause ?: e
- } finally {
- DebugProbes.uninstall()
- }
- }
-
- private fun handleTimeout(description: Description) {
- val units =
- if (testTimeoutMs % 1000 == 0L)
- "${testTimeoutMs / 1000} seconds"
- else "$testTimeoutMs milliseconds"
-
- System.err.println("\nTest ${description.methodName} timed out after $units\n")
- System.err.flush()
-
- DebugProbes.dumpCoroutines()
- System.out.flush() // Synchronize serr/sout
-
- /*
- * Order is important:
- * 1) Create exception with a stacktrace of hang test
- * 2) Cancel all coroutines via debug agent API (changing system state!)
- * 3) Throw created exception
- */
- val exception = createTimeoutException(testThread)
- cancelIfNecessary()
- // If timed out test throws an exception, we can't do much except ignoring it
- throw exception
- }
-
- private fun cancelIfNecessary() {
- if (cancelOnTimeout) {
- DebugProbes.dumpCoroutinesInfo().forEach {
- it.job?.cancel()
- }
- }
- }
-
- private fun createTimeoutException(thread: Thread): Exception {
- val stackTrace = thread.stackTrace
- val exception = TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS)
- exception.stackTrace = stackTrace
- thread.interrupt()
- return exception
- }
-}
diff --git a/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/kotlinx-coroutines-debug/test/DebugProbesTest.kt
index 01b2da00..4b394381 100644
--- a/kotlinx-coroutines-debug/test/DebugProbesTest.kt
+++ b/kotlinx-coroutines-debug/test/DebugProbesTest.kt
@@ -21,7 +21,6 @@ class DebugProbesTest : DebugTestBase() {
"java.util.concurrent.ExecutionException\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:14)\n" +
"\t(Coroutine boundary)\n" +
- "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:49)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:44)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsync\$1.invokeSuspend(DebugProbesTest.kt:17)\n",
@@ -42,7 +41,6 @@ class DebugProbesTest : DebugTestBase() {
"java.util.concurrent.ExecutionException\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt)\n" +
"\t(Coroutine boundary)\n" +
- "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsyncWithProbes\$1\$1.invokeSuspend(DebugProbesTest.kt:62)\n" +
@@ -74,7 +72,6 @@ class DebugProbesTest : DebugTestBase() {
"java.util.concurrent.ExecutionException\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" +
"\t(Coroutine boundary)\n" +
- "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" +
"\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsyncWithSanitizedProbes\$1\$1.invokeSuspend(DebugProbesTest.kt:87)\n" +
diff --git a/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt b/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt
index 67a283d0..fd1c2882 100644
--- a/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt
+++ b/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt
@@ -28,7 +28,6 @@ class SanitizedProbesTest : DebugTestBase() {
"java.util.concurrent.ExecutionException\n" +
"\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$createDeferredNested\$1.invokeSuspend(SanitizedProbesTest.kt:97)\n" +
"\t(Coroutine boundary)\n" +
- "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" +
"\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.oneMoreNestedMethod(SanitizedProbesTest.kt:67)\n" +
"\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.nestedMethod(SanitizedProbesTest.kt:61)\n" +
"\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testRecoveredStackTrace\$1.invokeSuspend(SanitizedProbesTest.kt:50)\n" +
diff --git a/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutExtensionTest.kt b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutExtensionTest.kt
new file mode 100644
index 00000000..752c6c35
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutExtensionTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+
+import kotlinx.coroutines.*
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.*
+import org.junit.jupiter.api.parallel.*
+
+class CoroutinesTimeoutExtensionTest {
+
+ /**
+ * Tests that disabling coroutine creation stacktraces in [CoroutinesTimeoutExtension] does lead to them not being
+ * created.
+ *
+ * Adapted from [CoroutinesTimeoutDisabledTracesTest], an identical test for the JUnit4 rule.
+ *
+ * This test class is not intended to be run manually. Instead, use [CoroutinesTimeoutTest] as the entry point.
+ */
+ class DisabledStackTracesTest {
+ @JvmField
+ @RegisterExtension
+ internal val timeout = CoroutinesTimeoutExtension(500, true, false)
+
+ private val job = GlobalScope.launch(Dispatchers.Unconfined) { hangForever() }
+
+ private suspend fun hangForever() {
+ suspendCancellableCoroutine<Unit> { }
+ expectUnreached()
+ }
+
+ @Test
+ fun hangingTest() = runBlocking<Unit> {
+ waitForHangJob()
+ expectUnreached()
+ }
+
+ private suspend fun waitForHangJob() {
+ job.join()
+ expectUnreached()
+ }
+ }
+
+ /**
+ * Tests that [CoroutinesTimeoutExtension] is installed eagerly and detects the coroutines that were launched before
+ * any test events start happening.
+ *
+ * Adapted from [CoroutinesTimeoutEagerTest], an identical test for the JUnit4 rule.
+ *
+ * This test class is not intended to be run manually. Instead, use [CoroutinesTimeoutTest] as the entry point.
+ */
+ class EagerTest {
+
+ @JvmField
+ @RegisterExtension
+ internal val timeout = CoroutinesTimeoutExtension(500)
+
+ private val job = GlobalScope.launch(Dispatchers.Unconfined) { hangForever() }
+
+ private suspend fun hangForever() {
+ suspendCancellableCoroutine<Unit> { }
+ expectUnreached()
+ }
+
+ @Test
+ fun hangingTest() = runBlocking<Unit> {
+ waitForHangJob()
+ expectUnreached()
+ }
+
+ private suspend fun waitForHangJob() {
+ job.join()
+ expectUnreached()
+ }
+ }
+
+ /**
+ * Tests that [CoroutinesTimeoutExtension] performs sensibly in some simple scenarios.
+ *
+ * Adapted from [CoroutinesTimeoutTest], an identical test for the JUnit4 rule.
+ *
+ * This test class is not intended to be run manually. Instead, use [CoroutinesTimeoutTest] as the entry point.
+ */
+ class SimpleTest {
+
+ @JvmField
+ @RegisterExtension
+ internal val timeout = CoroutinesTimeoutExtension(1000, false, true)
+
+ @Test
+ fun hangingTest() = runBlocking<Unit> {
+ suspendForever()
+ expectUnreached()
+ }
+
+ private suspend fun suspendForever() {
+ delay(Long.MAX_VALUE)
+ expectUnreached()
+ }
+
+ @Test
+ fun throwingTest() = runBlocking<Unit> {
+ throw RuntimeException()
+ }
+
+ @Test
+ fun successfulTest() = runBlocking {
+ val job = launch {
+ yield()
+ }
+
+ job.join()
+ }
+ }
+}
+
+private fun expectUnreached(): Nothing {
+ error("Should not be reached")
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutInheritanceTest.kt b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutInheritanceTest.kt
new file mode 100644
index 00000000..7c8de53d
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutInheritanceTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+
+import kotlinx.coroutines.*
+import org.junit.jupiter.api.*
+
+/**
+ * Tests that [CoroutinesTimeout] is inherited.
+ *
+ * This test class is not intended to be run manually. Instead, use [CoroutinesTimeoutTest] as the entry point.
+ */
+class CoroutinesTimeoutInheritanceTest {
+
+ @CoroutinesTimeout(100)
+ open class Base
+
+ @TestMethodOrder(MethodOrderer.OrderAnnotation::class)
+ class InheritedWithNoTimeout: Base() {
+
+ @Test
+ @Order(1)
+ fun usesBaseClassTimeout() = runBlocking {
+ delay(1000)
+ }
+
+ @CoroutinesTimeout(300)
+ @Test
+ @Order(2)
+ fun methodOverridesBaseClassTimeoutWithGreaterTimeout() = runBlocking {
+ delay(200)
+ }
+
+ @CoroutinesTimeout(10)
+ @Test
+ @Order(3)
+ fun methodOverridesBaseClassTimeoutWithLesserTimeout() = runBlocking {
+ delay(50)
+ }
+
+ }
+
+ @CoroutinesTimeout(300)
+ class InheritedWithGreaterTimeout : TestBase() {
+
+ @Test
+ fun classOverridesBaseClassTimeout1() = runBlocking {
+ delay(200)
+ }
+
+ @Test
+ fun classOverridesBaseClassTimeout2() = runBlocking {
+ delay(400)
+ }
+
+ }
+
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutMethodTest.kt b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutMethodTest.kt
new file mode 100644
index 00000000..64611b31
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutMethodTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+
+import kotlinx.coroutines.*
+import org.junit.jupiter.api.*
+
+/**
+ * Tests usage of [CoroutinesTimeout] on classes and test methods when only methods are annotated.
+ *
+ * This test class is not intended to be run manually. Instead, use [CoroutinesTimeoutTest] as the entry point.
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
+class CoroutinesTimeoutMethodTest {
+
+ @Test
+ @Order(1)
+ fun noClassTimeout() {
+ runBlocking {
+ delay(150)
+ }
+ }
+
+ @CoroutinesTimeout(100)
+ @Test
+ @Order(2)
+ fun usesMethodTimeoutWithNoClassTimeout() {
+ runBlocking {
+ delay(1000)
+ }
+ }
+
+ @CoroutinesTimeout(1000)
+ @Test
+ @Order(3)
+ fun fitsInMethodTimeout() {
+ runBlocking {
+ delay(10)
+ }
+ }
+
+}
diff --git a/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutNestedTest.kt b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutNestedTest.kt
new file mode 100644
index 00000000..04c933d0
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutNestedTest.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+
+import kotlinx.coroutines.*
+import org.junit.jupiter.api.*
+
+/**
+ * This test checks that nested classes correctly recognize the [CoroutinesTimeout] annotation.
+ *
+ * This test class is not intended to be run manually. Instead, use [CoroutinesTimeoutTest] as the entry point.
+ */
+@CoroutinesTimeout(200)
+class CoroutinesTimeoutNestedTest {
+ @Nested
+ inner class NestedInInherited {
+ @Test
+ fun usesOuterClassTimeout() = runBlocking {
+ delay(1000)
+ }
+
+ @Test
+ fun fitsInOuterClassTimeout() = runBlocking {
+ delay(10)
+ }
+ }
+}
diff --git a/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutSimpleTest.kt b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutSimpleTest.kt
new file mode 100644
index 00000000..513a8846
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutSimpleTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+
+import kotlinx.coroutines.*
+import org.junit.jupiter.api.*
+
+/**
+ * Tests the basic usage of [CoroutinesTimeout] on classes and test methods.
+ *
+ * This test class is not intended to be run manually. Instead, use [CoroutinesTimeoutTest] as the entry point.
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
+@CoroutinesTimeout(100)
+class CoroutinesTimeoutSimpleTest {
+
+ @Test
+ @Order(1)
+ fun usesClassTimeout1() {
+ runBlocking {
+ delay(150)
+ }
+ }
+
+ @CoroutinesTimeout(1000)
+ @Test
+ @Order(2)
+ fun ignoresClassTimeout() {
+ runBlocking {
+ delay(150)
+ }
+ }
+
+ @CoroutinesTimeout(200)
+ @Test
+ @Order(3)
+ fun usesMethodTimeout() {
+ runBlocking {
+ delay(300)
+ }
+ }
+
+ @Test
+ @Order(4)
+ fun fitsInClassTimeout() {
+ runBlocking {
+ delay(50)
+ }
+ }
+
+ @Test
+ @Order(5)
+ fun usesClassTimeout2() {
+ runBlocking {
+ delay(150)
+ }
+ }
+
+}
diff --git a/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutTest.kt b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutTest.kt
new file mode 100644
index 00000000..1f7b2080
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/junit5/CoroutinesTimeoutTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+
+import org.assertj.core.api.*
+import org.junit.Ignore
+import org.junit.Assert.*
+import org.junit.Test
+import org.junit.platform.engine.*
+import org.junit.platform.engine.discovery.DiscoverySelectors.*
+import org.junit.platform.testkit.engine.*
+import org.junit.platform.testkit.engine.EventConditions.*
+import java.io.*
+
+// note that these tests are run using JUnit4 in order not to mix the testing systems.
+class CoroutinesTimeoutTest {
+
+ // This test is ignored because it just checks an example.
+ @Test
+ @Ignore
+ fun testRegisterExtensionExample() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(selectClass(RegisterExtensionExample::class.java), capturedOut)
+ .testTimedOut("testThatHangs", 5000)
+ }
+
+ @Test
+ fun testCoroutinesTimeoutSimple() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(selectClass(CoroutinesTimeoutSimpleTest::class.java), capturedOut)
+ .testFinishedSuccessfully("ignoresClassTimeout")
+ .testFinishedSuccessfully("fitsInClassTimeout")
+ .testTimedOut("usesClassTimeout1", 100)
+ .testTimedOut("usesMethodTimeout", 200)
+ .testTimedOut("usesClassTimeout2", 100)
+ assertEquals(capturedOut.toString(), 3, countDumps(capturedOut))
+ }
+
+ @Test
+ fun testCoroutinesTimeoutMethod() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(selectClass(CoroutinesTimeoutMethodTest::class.java), capturedOut)
+ .testFinishedSuccessfully("fitsInMethodTimeout")
+ .testFinishedSuccessfully("noClassTimeout")
+ .testTimedOut("usesMethodTimeoutWithNoClassTimeout", 100)
+ assertEquals(capturedOut.toString(), 1, countDumps(capturedOut))
+ }
+
+ @Test
+ fun testCoroutinesTimeoutNested() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(selectClass(CoroutinesTimeoutNestedTest::class.java), capturedOut)
+ .testFinishedSuccessfully("fitsInOuterClassTimeout")
+ .testTimedOut("usesOuterClassTimeout", 200)
+ assertEquals(capturedOut.toString(), 1, countDumps(capturedOut))
+ }
+
+ @Test
+ fun testCoroutinesTimeoutInheritanceWithNoTimeoutInDerived() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(selectClass(CoroutinesTimeoutInheritanceTest.InheritedWithNoTimeout::class.java), capturedOut)
+ .testFinishedSuccessfully("methodOverridesBaseClassTimeoutWithGreaterTimeout")
+ .testTimedOut("usesBaseClassTimeout", 100)
+ .testTimedOut("methodOverridesBaseClassTimeoutWithLesserTimeout", 10)
+ assertEquals(capturedOut.toString(), 2, countDumps(capturedOut))
+ }
+
+ @Test
+ fun testCoroutinesTimeoutInheritanceWithGreaterTimeoutInDerived() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(
+ selectClass(CoroutinesTimeoutInheritanceTest.InheritedWithGreaterTimeout::class.java),
+ capturedOut
+ )
+ .testFinishedSuccessfully("classOverridesBaseClassTimeout1")
+ .testTimedOut("classOverridesBaseClassTimeout2", 300)
+ assertEquals(capturedOut.toString(), 1, countDumps(capturedOut))
+ }
+
+ /* Currently there's no ability to replicate [TestFailureValidation] as is for JUnit5:
+ https://github.com/junit-team/junit5/issues/506. So, the test mechanism is more ad-hoc. */
+
+ @Test
+ fun testCoroutinesTimeoutExtensionDisabledTraces() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(selectClass(CoroutinesTimeoutExtensionTest.DisabledStackTracesTest::class.java), capturedOut)
+ .testTimedOut("hangingTest", 500)
+ assertEquals(false, capturedOut.toString().contains("Coroutine creation stacktrace"))
+ assertEquals(capturedOut.toString(), 1, countDumps(capturedOut))
+ }
+
+ @Test
+ fun testCoroutinesTimeoutExtensionEager() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(selectClass(CoroutinesTimeoutExtensionTest.EagerTest::class.java), capturedOut)
+ .testTimedOut("hangingTest", 500)
+ for (expectedPart in listOf("hangForever", "waitForHangJob", "BlockingCoroutine{Active}")) {
+ assertEquals(expectedPart, true, capturedOut.toString().contains(expectedPart))
+ }
+ assertEquals(capturedOut.toString(), 1, countDumps(capturedOut))
+ }
+
+ @Test
+ fun testCoroutinesTimeoutExtensionSimple() {
+ val capturedOut = ByteArrayOutputStream()
+ eventsForSelector(selectClass(CoroutinesTimeoutExtensionTest.SimpleTest::class.java), capturedOut)
+ .testFinishedSuccessfully("successfulTest")
+ .testTimedOut("hangingTest", 1000)
+ .haveExactly(1, event(
+ test("throwingTest"),
+ finishedWithFailure(Condition({ it is RuntimeException}, "is RuntimeException"))
+ ))
+ for (expectedPart in listOf("suspendForever", "invokeSuspend", "BlockingCoroutine{Active}")) {
+ assertEquals(expectedPart, true, capturedOut.toString().contains(expectedPart))
+ }
+ for (nonExpectedPart in listOf("delay", "throwingTest")) {
+ assertEquals(nonExpectedPart, false, capturedOut.toString().contains(nonExpectedPart))
+ }
+ assertEquals(capturedOut.toString(), 1, countDumps(capturedOut))
+ }
+}
+
+private fun eventsForSelector(selector: DiscoverySelector, capturedOut: OutputStream): ListAssert<Event> {
+ val systemOut: PrintStream = System.out
+ val systemErr: PrintStream = System.err
+ return try {
+ System.setOut(PrintStream(capturedOut))
+ System.setErr(PrintStream(capturedOut))
+ EngineTestKit.engine("junit-jupiter")
+ .selectors(selector)
+ .execute()
+ .testEvents()
+ .assertThatEvents()
+ } finally {
+ System.setOut(systemOut)
+ System.setErr(systemErr)
+ }
+}
+
+private fun ListAssert<Event>.testFinishedSuccessfully(testName: String): ListAssert<Event> =
+ haveExactly(1, event(
+ test(testName),
+ finishedSuccessfully()
+ ))
+
+private fun ListAssert<Event>.testTimedOut(testName: String, after: Long): ListAssert<Event> =
+ haveExactly(1, event(
+ test(testName),
+ finishedWithFailure(Condition({ it is CoroutinesTimeoutException && it.timeoutMs == after },
+ "is CoroutinesTimeoutException($after)"))
+ ))
+
+/** Counts the number of occurrences of "Coroutines dump" in [capturedOut] */
+private fun countDumps(capturedOut: ByteArrayOutputStream): Int {
+ var result = 0
+ val outStr = capturedOut.toString()
+ val header = "Coroutines dump"
+ var i = 0
+ while (i < outStr.length - header.length) {
+ if (outStr.substring(i, i + header.length) == header) {
+ result += 1
+ i += header.length
+ } else {
+ i += 1
+ }
+ }
+ return result
+} \ No newline at end of file
diff --git a/kotlinx-coroutines-debug/test/junit5/RegisterExtensionExample.kt b/kotlinx-coroutines-debug/test/junit5/RegisterExtensionExample.kt
new file mode 100644
index 00000000..2de6b5b2
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/junit5/RegisterExtensionExample.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.debug.junit5
+
+import kotlinx.coroutines.*
+import org.junit.jupiter.api.*
+import org.junit.jupiter.api.extension.*
+
+class RegisterExtensionExample {
+ @JvmField
+ @RegisterExtension
+ internal val timeout = CoroutinesTimeoutExtension.seconds(5)
+
+ @Test
+ fun testThatHangs() = runBlocking {
+ delay(Long.MAX_VALUE) // somewhere deep in the stack
+ }
+} \ No newline at end of file