Skip to content

Commit 531d6a1

Browse files
committed
Extract common code for JUnit4 and JUnit5 CoroutinesTimeout
1 parent cd7afaf commit 531d6a1

File tree

9 files changed

+245
-186
lines changed

9 files changed

+245
-186
lines changed

kotlinx-coroutines-debug/build.gradle

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,12 @@ configurations {
1818
configureKotlinJvmPlatform(shadow)
1919
}
2020

21-
test {
22-
useJUnitPlatform()
23-
}
24-
2521
dependencies {
2622
compileOnly "junit:junit:$junit_version"
2723
compileOnly "org.junit.jupiter:junit-jupiter-api:$junit5_version"
2824
testCompile "org.junit.jupiter:junit-jupiter-api:$junit5_version"
2925
testCompile "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
30-
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit5_version"
26+
testCompile "org.junit.platform:junit-platform-testkit:1.7.0"
3127
shadowDeps "net.bytebuddy:byte-buddy:$byte_buddy_version"
3228
shadowDeps "net.bytebuddy:byte-buddy-agent:$byte_buddy_version"
3329
compileOnly "io.projectreactor.tools:blockhound:$blockhound_version"
@@ -46,6 +42,10 @@ if (rootProject.ext.jvm_ir_enabled) {
4642
}
4743
}
4844

45+
java {
46+
disableAutoTargetJvm()
47+
}
48+
4949
jar {
5050
manifest {
5151
attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain"
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.debug
6+
7+
import org.junit.runners.model.*
8+
import java.util.concurrent.*
9+
10+
/**
11+
* Run [invocation] in a separate thread with the given timeout in ms, after which the coroutines info is dumped and, if
12+
* [cancelOnTimeout] is set, the execution is interrupted.
13+
*
14+
* Assumes that [DebugProbes] are installed. Does not deinstall them.
15+
*/
16+
internal inline fun <T : Any?> runWithTimeoutDumpingCoroutines(
17+
methodName: String,
18+
testTimeoutMs: Long,
19+
cancelOnTimeout: Boolean,
20+
crossinline invocation: () -> T
21+
): T {
22+
val testStartedLatch = CountDownLatch(1)
23+
val testResult = FutureTask {
24+
testStartedLatch.countDown()
25+
invocation()
26+
}
27+
/*
28+
* We are using hand-rolled thread instead of single thread executor
29+
* in order to be able to safely interrupt thread in the end of a test
30+
*/
31+
val testThread = Thread(testResult, "Timeout test thread").apply { isDaemon = true }
32+
try {
33+
testThread.start()
34+
// Await until test is started to take only test execution time into account
35+
testStartedLatch.await()
36+
return testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS)
37+
} catch (e: TimeoutException) {
38+
handleTimeout(testThread, methodName, testTimeoutMs, cancelOnTimeout)
39+
} catch (e: ExecutionException) {
40+
throw e.cause ?: e
41+
}
42+
}
43+
44+
private fun handleTimeout(testThread: Thread, methodName: String, testTimeoutMs: Long, cancelOnTimeout: Boolean): Nothing {
45+
val units =
46+
if (testTimeoutMs % 1000 == 0L)
47+
"${testTimeoutMs / 1000} seconds"
48+
else "$testTimeoutMs milliseconds"
49+
50+
System.err.println("\nTest $methodName timed out after $units\n")
51+
System.err.flush()
52+
53+
DebugProbes.dumpCoroutines()
54+
System.out.flush() // Synchronize serr/sout
55+
56+
/*
57+
* Order is important:
58+
* 1) Create exception with a stacktrace of hang test
59+
* 2) Cancel all coroutines via debug agent API (changing system state!)
60+
* 3) Throw created exception
61+
*/
62+
val exception = createTimeoutException(testThread, testTimeoutMs)
63+
cancelIfNecessary(cancelOnTimeout)
64+
// If timed out test throws an exception, we can't do much except ignoring it
65+
throw exception
66+
}
67+
68+
private fun cancelIfNecessary(cancelOnTimeout: Boolean) {
69+
if (cancelOnTimeout) {
70+
DebugProbes.dumpCoroutinesInfo().forEach {
71+
it.job?.cancel()
72+
}
73+
}
74+
}
75+
76+
private fun createTimeoutException(thread: Thread, testTimeoutMs: Long): Exception {
77+
val stackTrace = thread.stackTrace
78+
val exception = TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS)
79+
exception.stackTrace = stackTrace
80+
thread.interrupt()
81+
return exception
82+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.debug.junit4
6+
7+
import kotlinx.coroutines.debug.*
8+
import org.junit.runner.*
9+
import org.junit.runners.model.*
10+
11+
internal class CoroutinesTimeoutStatement(
12+
private val testStatement: Statement,
13+
private val testDescription: Description,
14+
private val testTimeoutMs: Long,
15+
private val cancelOnTimeout: Boolean = false
16+
) : Statement() {
17+
18+
override fun evaluate() {
19+
try {
20+
runWithTimeoutDumpingCoroutines(testDescription.methodName, testTimeoutMs, cancelOnTimeout) {
21+
testStatement.evaluate()
22+
}
23+
} finally {
24+
DebugProbes.uninstall()
25+
}
26+
}
27+
}

kotlinx-coroutines-debug/src/junit5/CoroutinesTimeoutExtension.kt renamed to kotlinx-coroutines-debug/src/junit/junit5/CoroutinesTimeoutExtension.kt

Lines changed: 37 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,14 @@
44

55
package kotlinx.coroutines.debug.junit5
66

7-
import kotlinx.coroutines.ExperimentalCoroutinesApi
8-
import kotlinx.coroutines.debug.DebugProbes
9-
import org.junit.jupiter.api.extension.BeforeAllCallback
10-
import org.junit.jupiter.api.extension.ExtensionConfigurationException
7+
import kotlinx.coroutines.debug.*
8+
import kotlinx.coroutines.debug.runWithTimeoutDumpingCoroutines
119
import org.junit.jupiter.api.extension.ExtensionContext
1210
import org.junit.jupiter.api.extension.InvocationInterceptor
1311
import org.junit.jupiter.api.extension.ReflectiveInvocationContext
14-
import org.junit.platform.commons.JUnitException
1512
import org.junit.platform.commons.support.AnnotationSupport
16-
import org.junit.runner.*
1713
import org.junit.runners.model.*
18-
import java.io.ByteArrayOutputStream
19-
import java.io.PrintStream
2014
import java.lang.reflect.Method
21-
import java.time.Duration
22-
import java.time.format.DateTimeParseException
2315
import java.util.concurrent.CountDownLatch
2416
import java.util.concurrent.ExecutionException
2517
import java.util.concurrent.FutureTask
@@ -33,132 +25,93 @@ public class CoroutinesTimeoutExtension: InvocationInterceptor {
3325
invocationContext: ReflectiveInvocationContext<Method>,
3426
extensionContext: ExtensionContext
3527
) {
36-
val annotation =
37-
AnnotationSupport.findAnnotation(invocationContext.executable, CoroutinesTimeout::class.java).or {
38-
AnnotationSupport.findAnnotation(extensionContext.testClass, CoroutinesTimeout::class.java)
39-
}.orElseGet {
40-
throw UnsupportedOperationException("CoroutinesTimeoutExtension should not be used directly; annotate the test class or method with CoroutinesTimeout instead.")
41-
}
42-
interceptInvocation(invocation, invocationContext.executable.name, annotation)
28+
interceptNormalMethod(invocation, invocationContext, extensionContext)
4329
}
4430

4531
override fun interceptAfterAllMethod(
4632
invocation: InvocationInterceptor.Invocation<Void>,
4733
invocationContext: ReflectiveInvocationContext<Method>,
4834
extensionContext: ExtensionContext
4935
) {
50-
interceptLifecycleMethod(invocation, invocationContext, extensionContext)
36+
interceptLifecycleMethod(invocation, invocationContext)
5137
}
5238

5339
override fun interceptAfterEachMethod(
5440
invocation: InvocationInterceptor.Invocation<Void>,
5541
invocationContext: ReflectiveInvocationContext<Method>,
5642
extensionContext: ExtensionContext
5743
) {
58-
interceptLifecycleMethod(invocation, invocationContext, extensionContext)
44+
interceptLifecycleMethod(invocation, invocationContext)
5945
}
6046

6147
override fun interceptBeforeAllMethod(
6248
invocation: InvocationInterceptor.Invocation<Void>,
6349
invocationContext: ReflectiveInvocationContext<Method>,
6450
extensionContext: ExtensionContext
6551
) {
66-
interceptLifecycleMethod(invocation, invocationContext, extensionContext)
52+
interceptLifecycleMethod(invocation, invocationContext)
6753
}
6854

6955
override fun interceptBeforeEachMethod(
7056
invocation: InvocationInterceptor.Invocation<Void>,
7157
invocationContext: ReflectiveInvocationContext<Method>,
7258
extensionContext: ExtensionContext
7359
) {
74-
interceptLifecycleMethod(invocation, invocationContext, extensionContext)
60+
interceptLifecycleMethod(invocation, invocationContext)
7561
}
7662

77-
private fun interceptLifecycleMethod(
63+
override fun <T : Any?> interceptTestFactoryMethod(
64+
invocation: InvocationInterceptor.Invocation<T>,
65+
invocationContext: ReflectiveInvocationContext<Method>,
66+
extensionContext: ExtensionContext
67+
): T = interceptNormalMethod(invocation, invocationContext, extensionContext)
68+
69+
override fun interceptTestTemplateMethod(
7870
invocation: InvocationInterceptor.Invocation<Void>,
7971
invocationContext: ReflectiveInvocationContext<Method>,
8072
extensionContext: ExtensionContext
8173
) {
82-
invocation.proceed()
74+
interceptNormalMethod(invocation, invocationContext, extensionContext)
8375
}
8476

85-
override fun <T : Any?> interceptTestFactoryMethod(
77+
private fun <T: Any?> interceptNormalMethod(
8678
invocation: InvocationInterceptor.Invocation<T>,
8779
invocationContext: ReflectiveInvocationContext<Method>,
8880
extensionContext: ExtensionContext
89-
): T = invocation.proceed()
81+
): T {
82+
val annotation =
83+
AnnotationSupport.findAnnotation(invocationContext.executable, CoroutinesTimeout::class.java).or {
84+
AnnotationSupport.findAnnotation(extensionContext.testClass, CoroutinesTimeout::class.java)
85+
}.orElseGet {
86+
throw UnsupportedOperationException("CoroutinesTimeoutExtension should not be used directly; annotate the test class or method with CoroutinesTimeout instead.")
87+
}
88+
return interceptInvocation(invocation, invocationContext.executable.name, annotation)
89+
}
9090

91-
override fun interceptTestTemplateMethod(
91+
private fun interceptLifecycleMethod(
9292
invocation: InvocationInterceptor.Invocation<Void>,
93-
invocationContext: ReflectiveInvocationContext<Method>,
94-
extensionContext: ExtensionContext
93+
invocationContext: ReflectiveInvocationContext<Method>
9594
) {
96-
invocation.proceed()
95+
val annotation =
96+
AnnotationSupport.findAnnotation(invocationContext.executable, CoroutinesTimeout::class.java).orElseGet {
97+
throw UnsupportedOperationException("CoroutinesTimeoutExtension should not be used directly; annotate the test class or method with CoroutinesTimeout instead.")
98+
}
99+
interceptInvocation(invocation, invocationContext.executable.name, annotation)
97100
}
98101

99102
private fun <T : Any?> interceptInvocation(
100103
invocation: InvocationInterceptor.Invocation<T>,
101104
methodName: String,
102105
annotation: CoroutinesTimeout
103106
): T {
104-
val testStartedLatch = CountDownLatch(1)
105-
val testResult = FutureTask<T> {
106-
testStartedLatch.countDown()
107-
invocation.proceed()
108-
}
109-
val testThread = Thread(testResult, "Timeout test thread").apply { isDaemon = true }
107+
DebugProbes.enableCreationStackTraces = annotation.enableCoroutineCreationStackTraces
110108
DebugProbes.install()
111-
try {
112-
testThread.start()
113-
// Await until test is started to take only test execution time into account
114-
testStartedLatch.await()
115-
return testResult.get(annotation.testTimeoutMs, TimeUnit.MILLISECONDS)
116-
} catch (e: TimeoutException) {
117-
handleTimeout(methodName, testThread, annotation)
118-
} catch (e: ExecutionException) {
119-
throw e.cause ?: e
109+
return try {
110+
runWithTimeoutDumpingCoroutines(methodName, annotation.testTimeoutMs, annotation.cancelOnTimeout) {
111+
invocation.proceed()
112+
}
120113
} finally {
121114
DebugProbes.uninstall()
122115
}
123116
}
124-
125-
private fun handleTimeout(methodName: String, testThread: Thread, annotation: CoroutinesTimeout): Nothing {
126-
val units =
127-
if (annotation.testTimeoutMs % 1000 == 0L)
128-
"${annotation.testTimeoutMs / 1000} seconds"
129-
else "$annotation.testTimeoutMs milliseconds"
130-
131-
System.err.println("\nTest $methodName timed out after $units\n")
132-
System.err.flush()
133-
134-
DebugProbes.dumpCoroutines()
135-
System.out.flush() // Synchronize serr/sout
136-
137-
/*
138-
* Order is important:
139-
* 1) Create exception with a stacktrace of hang test
140-
* 2) Cancel all coroutines via debug agent API (changing system state!)
141-
* 3) Throw created exception
142-
*/
143-
val exception = createTimeoutException(testThread, annotation.testTimeoutMs)
144-
cancelIfNecessary(annotation.cancelOnTimeout)
145-
// If timed out test throws an exception, we can't do much except ignoring it
146-
throw exception
147-
}
148-
149-
private fun cancelIfNecessary(cancelOnTimeout: Boolean) {
150-
if (cancelOnTimeout) {
151-
DebugProbes.dumpCoroutinesInfo().forEach {
152-
it.job?.cancel()
153-
}
154-
}
155-
}
156-
157-
private fun createTimeoutException(thread: Thread, testTimeoutMs: Long): Exception {
158-
val stackTrace = thread.stackTrace
159-
val exception = TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS)
160-
exception.stackTrace = stackTrace
161-
thread.interrupt()
162-
return exception
163-
}
164117
}

0 commit comments

Comments
 (0)