Skip to content

Commit 70e3583

Browse files
committed
Make CoroutinesDumpTest deterministic
1 parent 12a0318 commit 70e3583

File tree

3 files changed

+97
-68
lines changed

3 files changed

+97
-68
lines changed

kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt

+1
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ internal class CoroutineScheduler(
653653
}
654654

655655
override fun run() = runWorker()
656+
656657
@JvmField
657658
var mayHaveLocalTasks = false
658659

kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt

+85-66
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,47 @@ import kotlin.test.*
1111

1212
class CoroutinesDumpTest : DebugTestBase() {
1313
private val monitor = Any()
14-
private var coroutineStarted = false // guarded by monitor
14+
private var coroutineThread: Thread? = null // guarded by monitor
1515

1616
@Test
17-
fun testSuspendedCoroutine() = synchronized(monitor) {
18-
val deferred = GlobalScope.async {
17+
fun testSuspendedCoroutine() = runBlocking {
18+
val deferred = async(Dispatchers.Default) {
1919
sleepingOuterMethod()
2020
}
2121

22-
awaitCoroutineStarted()
23-
Thread.sleep(100) // Let delay be invoked
22+
awaitCoroutine()
23+
val found = DebugProbes.dumpCoroutinesInfo().single { it.job === deferred }
2424
verifyDump(
2525
"Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: SUSPENDED\n" +
26-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingNestedMethod(CoroutinesDumpTest.kt:95)\n" +
27-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingOuterMethod(CoroutinesDumpTest.kt:88)\n" +
28-
"\t(Coroutine creation stacktrace)\n" +
29-
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
30-
"\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
31-
"\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n")
32-
33-
val found = DebugProbes.dumpCoroutinesInfo().single { it.job === deferred }
26+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingNestedMethod(CoroutinesDumpTest.kt:95)\n" +
27+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingOuterMethod(CoroutinesDumpTest.kt:88)\n" +
28+
"\t(Coroutine creation stacktrace)\n" +
29+
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
30+
"\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
31+
"\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n",
32+
ignoredCoroutine = "BlockingCoroutine"
33+
) {
34+
deferred.cancel()
35+
coroutineThread!!.interrupt()
36+
}
3437
assertSame(deferred, found.job)
35-
runBlocking { deferred.cancelAndJoin() }
3638
}
3739

3840
@Test
39-
fun testRunningCoroutine() = synchronized(monitor) {
40-
val deferred = GlobalScope.async {
41+
fun testRunningCoroutine() = runBlocking {
42+
val deferred = async(Dispatchers.Default) {
4143
activeMethod(shouldSuspend = false)
4244
assertTrue(true)
4345
}
4446

45-
awaitCoroutineStarted()
47+
awaitCoroutine()
4648
verifyDump(
47-
"Coroutine \"coroutine#1\":DeferredCoroutine{Active}@227d9994, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" +
48-
"\t(Coroutine creation stacktrace)\n" +
49+
"Coroutine \"coroutine#1\":DeferredCoroutine{Active}@227d9994, state: RUNNING\n" +
50+
"\tat java.lang.Thread.sleep(Native Method)\n" +
51+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:141)\n" +
52+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:133)\n" +
53+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutine\$1$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:41)\n" +
54+
"\t(Coroutine creation stacktrace)\n" +
4955
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
5056
"\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
5157
"\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" +
@@ -54,74 +60,84 @@ class CoroutinesDumpTest : DebugTestBase() {
5460
"\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" +
5561
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" +
5662
"\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" +
57-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutine(CoroutinesDumpTest.kt:49)")
58-
runBlocking { deferred.cancelAndJoin() }
63+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutine(CoroutinesDumpTest.kt:49)",
64+
ignoredCoroutine = "BlockingCoroutine"
65+
) {
66+
deferred.cancel()
67+
coroutineThread?.interrupt()
68+
}
5969
}
6070

6171
@Test
62-
fun testRunningCoroutineWithSuspensionPoint() = synchronized(monitor) {
63-
val deferred = GlobalScope.async {
72+
fun testRunningCoroutineWithSuspensionPoint() = runBlocking {
73+
val deferred = async(Dispatchers.Default) {
6474
activeMethod(shouldSuspend = true)
6575
yield() // tail-call
6676
}
6777

68-
awaitCoroutineStarted()
69-
Thread.sleep(10)
78+
awaitCoroutine()
7079
verifyDump(
7180
"Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING\n" +
72-
"\tat java.lang.Thread.sleep(Native Method)\n" +
73-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" +
74-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" +
75-
"\t(Coroutine creation stacktrace)\n" +
76-
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
77-
"\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
78-
"\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" +
79-
"\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" +
80-
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" +
81-
"\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" +
82-
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" +
83-
"\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" +
84-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt:71)"
85-
)
86-
runBlocking { deferred.cancelAndJoin() }
81+
"\tat java.lang.Thread.sleep(Native Method)\n" +
82+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" +
83+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" +
84+
"\t(Coroutine creation stacktrace)\n" +
85+
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
86+
"\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
87+
"\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" +
88+
"\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" +
89+
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" +
90+
"\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" +
91+
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" +
92+
"\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" +
93+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt:71)",
94+
ignoredCoroutine = "BlockingCoroutine"
95+
) {
96+
deferred.cancel()
97+
coroutineThread!!.interrupt()
98+
}
8799
}
88100

89101
@Test
90-
fun testCreationStackTrace() = synchronized(monitor) {
91-
val deferred = GlobalScope.async {
102+
fun testCreationStackTrace() = runBlocking {
103+
val deferred = async(Dispatchers.Default) {
92104
activeMethod(shouldSuspend = true)
93105
}
94106

95-
awaitCoroutineStarted()
96-
val coroutine = DebugProbes.dumpCoroutinesInfo().first()
107+
awaitCoroutine()
108+
val coroutine = DebugProbes.dumpCoroutinesInfo().first { it.job is Deferred<*> }
97109
val result = coroutine.creationStackTrace.fold(StringBuilder()) { acc, element ->
98110
acc.append(element.toString())
99111
acc.append('\n')
100112
}.toString().trimStackTrace()
101113

102-
runBlocking { deferred.cancelAndJoin() }
103-
104-
val expected = ("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
105-
"kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
106-
"kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)\n" +
107-
"kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)\n" +
108-
"kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)\n" +
109-
"kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" +
110-
"kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt:81)\n" +
111-
"kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" +
112-
"kotlinx.coroutines.debug.CoroutinesDumpTest.testCreationStackTrace(CoroutinesDumpTest.kt:109)").trimStackTrace()
114+
deferred.cancel()
115+
coroutineThread!!.interrupt()
116+
117+
val expected =
118+
("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
119+
"kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
120+
"kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)\n" +
121+
"kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)\n" +
122+
"kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)\n" +
123+
"kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" +
124+
"kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt:81)\n" +
125+
"kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" +
126+
"kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)").trimStackTrace()
113127
assertTrue(result.startsWith(expected))
114128
}
115129

116130
@Test
117-
fun testFinishedCoroutineRemoved() = synchronized(monitor) {
118-
val deferred = GlobalScope.async {
131+
fun testFinishedCoroutineRemoved() = runBlocking {
132+
val deferred = async(Dispatchers.Default) {
119133
activeMethod(shouldSuspend = true)
120134
}
121135

122-
awaitCoroutineStarted()
123-
runBlocking { deferred.cancelAndJoin() }
124-
verifyDump()
136+
awaitCoroutine()
137+
deferred.cancel()
138+
coroutineThread!!.interrupt()
139+
deferred.join()
140+
verifyDump(ignoredCoroutine = "BlockingCoroutine")
125141
}
126142

127143
private suspend fun activeMethod(shouldSuspend: Boolean) {
@@ -133,28 +149,31 @@ class CoroutinesDumpTest : DebugTestBase() {
133149
if (shouldSuspend) yield()
134150
notifyCoroutineStarted()
135151
while (coroutineContext[Job]!!.isActive) {
136-
Thread.sleep(100)
152+
runCatching { Thread.sleep(60_000) }
137153
}
138154
}
139155

140156
private suspend fun sleepingOuterMethod() {
141157
sleepingNestedMethod()
142-
yield()
158+
yield() // TCE
143159
}
144160

145161
private suspend fun sleepingNestedMethod() {
146-
yield()
162+
yield() // Suspension point
147163
notifyCoroutineStarted()
148164
delay(Long.MAX_VALUE)
149165
}
150166

151-
private fun awaitCoroutineStarted() {
152-
while (!coroutineStarted) (monitor as Object).wait()
167+
private fun awaitCoroutine() = synchronized(monitor) {
168+
while (coroutineThread == null) (monitor as Object).wait()
169+
while (coroutineThread!!.state != Thread.State.TIMED_WAITING) {
170+
// Wait until thread sleeps to have a consistent stacktrace
171+
}
153172
}
154173

155174
private fun notifyCoroutineStarted() {
156175
synchronized(monitor) {
157-
coroutineStarted = true
176+
coroutineThread = Thread.currentThread()
158177
(monitor as Object).notifyAll()
159178
}
160179
}

kotlinx-coroutines-debug/test/StracktraceUtils.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,22 @@ public fun toStackTrace(t: Throwable): String {
5555

5656
public fun String.count(substring: String): Int = split(substring).size - 1
5757

58+
public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null, finally: () -> Unit) {
59+
try {
60+
verifyDump(*traces, ignoredCoroutine = ignoredCoroutine)
61+
} finally {
62+
finally()
63+
}
64+
}
65+
5866
public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) {
5967
val baos = ByteArrayOutputStream()
6068
DebugProbes.dumpCoroutines(PrintStream(baos))
6169
val trace = baos.toString().split("\n\n")
6270
if (traces.isEmpty()) {
63-
assertEquals(1, trace.size)
64-
assertTrue(trace[0].startsWith("Coroutines dump"))
71+
val filtered = trace.filter { ignoredCoroutine == null || !it.contains(ignoredCoroutine) }
72+
assertEquals(1, filtered.count())
73+
assertTrue(filtered[0].startsWith("Coroutines dump"))
6574
return
6675
}
6776
// Drop "Coroutine dump" line

0 commit comments

Comments
 (0)