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