• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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