4
4
5
5
package kotlinx.coroutines.debug.junit5
6
6
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
11
9
import org.junit.jupiter.api.extension.ExtensionContext
12
10
import org.junit.jupiter.api.extension.InvocationInterceptor
13
11
import org.junit.jupiter.api.extension.ReflectiveInvocationContext
14
- import org.junit.platform.commons.JUnitException
15
12
import org.junit.platform.commons.support.AnnotationSupport
16
- import org.junit.runner.*
17
13
import org.junit.runners.model.*
18
- import java.io.ByteArrayOutputStream
19
- import java.io.PrintStream
20
14
import java.lang.reflect.Method
21
- import java.time.Duration
22
- import java.time.format.DateTimeParseException
23
15
import java.util.concurrent.CountDownLatch
24
16
import java.util.concurrent.ExecutionException
25
17
import java.util.concurrent.FutureTask
@@ -33,132 +25,93 @@ public class CoroutinesTimeoutExtension: InvocationInterceptor {
33
25
invocationContext : ReflectiveInvocationContext <Method >,
34
26
extensionContext : ExtensionContext
35
27
) {
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)
43
29
}
44
30
45
31
override fun interceptAfterAllMethod (
46
32
invocation : InvocationInterceptor .Invocation <Void >,
47
33
invocationContext : ReflectiveInvocationContext <Method >,
48
34
extensionContext : ExtensionContext
49
35
) {
50
- interceptLifecycleMethod(invocation, invocationContext, extensionContext )
36
+ interceptLifecycleMethod(invocation, invocationContext)
51
37
}
52
38
53
39
override fun interceptAfterEachMethod (
54
40
invocation : InvocationInterceptor .Invocation <Void >,
55
41
invocationContext : ReflectiveInvocationContext <Method >,
56
42
extensionContext : ExtensionContext
57
43
) {
58
- interceptLifecycleMethod(invocation, invocationContext, extensionContext )
44
+ interceptLifecycleMethod(invocation, invocationContext)
59
45
}
60
46
61
47
override fun interceptBeforeAllMethod (
62
48
invocation : InvocationInterceptor .Invocation <Void >,
63
49
invocationContext : ReflectiveInvocationContext <Method >,
64
50
extensionContext : ExtensionContext
65
51
) {
66
- interceptLifecycleMethod(invocation, invocationContext, extensionContext )
52
+ interceptLifecycleMethod(invocation, invocationContext)
67
53
}
68
54
69
55
override fun interceptBeforeEachMethod (
70
56
invocation : InvocationInterceptor .Invocation <Void >,
71
57
invocationContext : ReflectiveInvocationContext <Method >,
72
58
extensionContext : ExtensionContext
73
59
) {
74
- interceptLifecycleMethod(invocation, invocationContext, extensionContext )
60
+ interceptLifecycleMethod(invocation, invocationContext)
75
61
}
76
62
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 (
78
70
invocation : InvocationInterceptor .Invocation <Void >,
79
71
invocationContext : ReflectiveInvocationContext <Method >,
80
72
extensionContext : ExtensionContext
81
73
) {
82
- invocation.proceed( )
74
+ interceptNormalMethod(invocation, invocationContext, extensionContext )
83
75
}
84
76
85
- override fun <T : Any ?> interceptTestFactoryMethod (
77
+ private fun <T : Any ?> interceptNormalMethod (
86
78
invocation : InvocationInterceptor .Invocation <T >,
87
79
invocationContext : ReflectiveInvocationContext <Method >,
88
80
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
+ }
90
90
91
- override fun interceptTestTemplateMethod (
91
+ private fun interceptLifecycleMethod (
92
92
invocation : InvocationInterceptor .Invocation <Void >,
93
- invocationContext : ReflectiveInvocationContext <Method >,
94
- extensionContext : ExtensionContext
93
+ invocationContext : ReflectiveInvocationContext <Method >
95
94
) {
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)
97
100
}
98
101
99
102
private fun <T : Any ?> interceptInvocation (
100
103
invocation : InvocationInterceptor .Invocation <T >,
101
104
methodName : String ,
102
105
annotation : CoroutinesTimeout
103
106
): 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
110
108
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
+ }
120
113
} finally {
121
114
DebugProbes .uninstall()
122
115
}
123
116
}
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 (" \n Test $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
- }
164
117
}
0 commit comments