<lambda>null1 package kotlinx.coroutines.debug
2
3 import java.io.*
4 import kotlin.test.*
5
6 public fun String.trimStackTrace(): String =
7 trimIndent()
8 // Remove source line
9 .replace(Regex(":[0-9]+"), "")
10 // Remove coroutine id
11 .replace(Regex("#[0-9]+"), "")
12 // Remove trace prefix: "[email protected]/java.lang.Thread.sleep" => "java.lang.Thread.sleep"
13 .replace(Regex("(?<=\tat )[^\n]*/"), "")
14 .replace(Regex("\t"), "")
15 .replace("sun.misc.Unsafe.", "jdk.internal.misc.Unsafe.") // JDK8->JDK11
16
17 public fun verifyStackTrace(e: Throwable, traces: List<String>) {
18 val stacktrace = toStackTrace(e)
19 val trimmedStackTrace = stacktrace.trimStackTrace()
20 traces.forEach {
21 assertTrue(
22 trimmedStackTrace.contains(it.trimStackTrace()),
23 "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace"
24 )
25 }
26
27 val causes = stacktrace.count("Caused by")
28 assertNotEquals(0, causes)
29 assertEquals(causes, traces.map { it.count("Caused by") }.sum())
30 }
31
toStackTracenull32 public fun toStackTrace(t: Throwable): String {
33 val sw = StringWriter()
34 t.printStackTrace(PrintWriter(sw))
35 return sw.toString()
36 }
37
countnull38 public fun String.count(substring: String): Int = split(substring).size - 1
39
40 public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null, finally: () -> Unit) {
41 try {
42 verifyDump(*traces, ignoredCoroutine = ignoredCoroutine)
43 } finally {
44 finally()
45 }
46 }
47
48 /** Clean the stacktraces from artifacts of BlockHound instrumentation
49 *
50 * BlockHound works by switching a native call by a class generated with ByteBuddy, which, if the blocking
51 * call is allowed in this context, in turn calls the real native call that is now available under a
52 * different name.
53 *
54 * The traces thus undergo the following two changes when the execution is instrumented:
55 * - The original native call is replaced with a non-native one with the same FQN, and
56 * - An additional native call is placed on top of the stack, with the original name that also has
57 * `$$BlockHound$$_` prepended at the last component.
58 */
cleanBlockHoundTracesnull59 private fun cleanBlockHoundTraces(frames: List<String>): List<String> {
60 val result = mutableListOf<String>()
61 val blockHoundSubstr = "\$\$BlockHound\$\$_"
62 var i = 0
63 while (i < frames.size) {
64 result.add(frames[i].replace(blockHoundSubstr, ""))
65 if (frames[i].contains(blockHoundSubstr)) {
66 i += 1
67 }
68 i += 1
69 }
70 return result
71 }
72
73 /**
74 * Removes all frames that contain "java.util.concurrent" in it.
75 *
76 * We do leverage Java's locks for proper rendezvous and to fix the coroutine stack's state,
77 * but this API doesn't have (nor expected to) stable stacktrace, so we are filtering all such
78 * frames out.
79 *
80 * See https://github.com/Kotlin/kotlinx.coroutines/issues/3700 for the example of failure
81 */
removeJavaUtilConcurrentTracesnull82 private fun removeJavaUtilConcurrentTraces(frames: List<String>): List<String> =
83 frames.filter { !it.contains("java.util.concurrent") }
84
85 private data class CoroutineDump(
86 val header: CoroutineDumpHeader,
87 val coroutineStackTrace: List<String>,
88 val threadStackTrace: List<String>,
89 val originDump: String,
90 val originHeader: String,
91 ) {
92 companion object {
93 private val COROUTINE_CREATION_FRAME_REGEX =
94 "at _COROUTINE\\._CREATION\\._\\(.*\\)".toRegex()
95
parsenull96 fun parse(dump: String, traceCleaner: ((List<String>) -> List<String>)? = null): CoroutineDump {
97 val lines = dump
98 .trimStackTrace()
99 .split("\n")
100 val header = CoroutineDumpHeader.parse(lines[0])
101 val traceLines = lines.slice(1 until lines.size)
102 val cleanedTraceLines = if (traceCleaner != null) {
103 traceCleaner(traceLines)
104 } else {
105 traceLines
106 }
107 val coroutineStackTrace = mutableListOf<String>()
108 val threadStackTrace = mutableListOf<String>()
109 var trace = coroutineStackTrace
110 for (line in cleanedTraceLines) {
111 if (line.isEmpty()) {
112 continue
113 }
114 if (line.matches(COROUTINE_CREATION_FRAME_REGEX)) {
115 require(trace !== threadStackTrace) {
116 "Found more than one coroutine creation frame"
117 }
118 trace = threadStackTrace
119 continue
120 }
121 trace.add(line)
122 }
123 return CoroutineDump(header, coroutineStackTrace, threadStackTrace, dump, lines[0])
124 }
125 }
126
verifynull127 fun verify(expected: CoroutineDump) {
128 assertEquals(
129 expected.header, header,
130 "Coroutine stacktrace headers are not matched:\n\t- ${expected.originHeader}\n\t+ ${originHeader}\n"
131 )
132 verifyStackTrace("coroutine stack", coroutineStackTrace, expected.coroutineStackTrace)
133 verifyStackTrace("thread stack", threadStackTrace, expected.threadStackTrace)
134 }
135
verifyStackTracenull136 private fun verifyStackTrace(traceName: String, actualStackTrace: List<String>, expectedStackTrace: List<String>) {
137 // It is possible there are more stack frames in a dump than we check
138 for ((ix, expectedLine) in expectedStackTrace.withIndex()) {
139 val actualLine = actualStackTrace[ix]
140 assertEquals(
141 expectedLine, actualLine,
142 "Following lines from $traceName are not matched:\n\t- ${expectedLine}\n\t+ ${actualLine}\nActual dump:\n$originDump\n\n"
143 )
144 }
145 }
146 }
147
148 private data class CoroutineDumpHeader(
149 val name: String?,
150 val className: String,
151 val state: String,
152 ) {
153 companion object {
154 /**
155 * Parses following strings:
156 *
157 * - Coroutine "coroutine#10":DeferredCoroutine{Active}@66d87651, state: RUNNING
158 * - Coroutine DeferredCoroutine{Active}@66d87651, state: RUNNING
159 *
160 * into:
161 *
162 * - `CoroutineDumpHeader(name = "coroutine", className = "DeferredCoroutine", state = "RUNNING")`
163 * - `CoroutineDumpHeader(name = null, className = "DeferredCoroutine", state = "RUNNING")`
164 */
parsenull165 fun parse(header: String): CoroutineDumpHeader {
166 val (identFull, stateFull) = header.split(", ", limit = 2)
167 val nameAndClassName = identFull.removePrefix("Coroutine ").split('@', limit = 2)[0]
168 val (name, className) = nameAndClassName.split(':', limit = 2).let { parts ->
169 val (quotedName, classNameWithState) = if (parts.size == 1) {
170 null to parts[0]
171 } else {
172 parts[0] to parts[1]
173 }
174 val name = quotedName?.removeSurrounding("\"")?.split('#', limit = 2)?.get(0)
175 val className = classNameWithState.replace("\\{.*\\}".toRegex(), "")
176 name to className
177 }
178 val state = stateFull.removePrefix("state: ")
179 return CoroutineDumpHeader(name, className, state)
180 }
181 }
182 }
183
verifyDumpnull184 public fun verifyDump(vararg expectedTraces: String, ignoredCoroutine: String? = null) {
185 val baos = ByteArrayOutputStream()
186 DebugProbes.dumpCoroutines(PrintStream(baos))
187 val wholeDump = baos.toString()
188 val traces = wholeDump.split("\n\n")
189 assertTrue(traces[0].startsWith("Coroutines dump"))
190
191 val dumps = traces
192 // Drop "Coroutine dump" line
193 .drop(1)
194 // Parse dumps and filter out ignored coroutines
195 .mapNotNull { trace ->
196 val dump = CoroutineDump.parse(trace, {
197 removeJavaUtilConcurrentTraces(cleanBlockHoundTraces(it))
198 })
199 if (dump.header.className == ignoredCoroutine) {
200 null
201 } else {
202 dump
203 }
204 }
205
206 assertEquals(expectedTraces.size, dumps.size)
207 dumps.zip(expectedTraces.map { CoroutineDump.parse(it, ::removeJavaUtilConcurrentTraces) })
208 .forEach { (dump, expectedDump) ->
209 dump.verify(expectedDump)
210 }
211 }
212
trimPackagenull213 public fun String.trimPackage() = replace("kotlinx.coroutines.debug.", "")
214
215 public fun verifyPartialDump(createdCoroutinesCount: Int, vararg frames: String) {
216 val baos = ByteArrayOutputStream()
217 DebugProbes.dumpCoroutines(PrintStream(baos))
218 val dump = baos.toString()
219 val trace = dump.split("\n\n")
220 val matches = frames.all { frame ->
221 trace.any { tr -> tr.contains(frame) }
222 }
223
224 assertEquals(createdCoroutinesCount, DebugProbes.dumpCoroutinesInfo().size)
225 assertTrue(matches)
226 }
227