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         .replace(Regex(":[0-9]+"), "")
13         .replace(Regex("#[0-9]+"), "")
14         .replace(Regex("(?<=\tat )[^\n]*/"), "")
15         .replace(Regex("\t"), "")
16         .replace("sun.misc.Unsafe.", "jdk.internal.misc.Unsafe.") // JDK8->JDK11
17         .applyBackspace()
18 
19 public fun String.applyBackspace(): String {
20     val array = toCharArray()
21     val stack = CharArray(array.size)
22     var stackSize = -1
23     for (c in array) {
24         if (c != '\b') {
25             stack[++stackSize] = c
26         } else {
27             --stackSize
28         }
29     }
30 
31     return String(stack, 0, stackSize + 1)
32 }
33 
verifyStackTracenull34 public fun verifyStackTrace(e: Throwable, traces: List<String>) {
35     val stacktrace = toStackTrace(e)
36     val trimmedStackTrace = stacktrace.trimStackTrace()
37     traces.forEach {
38         assertTrue(
39             trimmedStackTrace.contains(it.trimStackTrace()),
40             "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace"
41         )
42     }
43 
44     val causes = stacktrace.count("Caused by")
45     assertNotEquals(0, causes)
46     assertEquals(causes, traces.map { it.count("Caused by") }.sum())
47 }
48 
toStackTracenull49 public fun toStackTrace(t: Throwable): String {
50     val sw = StringWriter()
51     t.printStackTrace(PrintWriter(sw))
52     return sw.toString()
53 }
54 
countnull55 public fun String.count(substring: String): Int = split(substring).size - 1
56 
57 public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null, finally: () -> Unit) {
58     try {
59         verifyDump(*traces, ignoredCoroutine = ignoredCoroutine)
60     } finally {
61         finally()
62     }
63 }
64 
65 /** Clean the stacktraces from artifacts of BlockHound instrumentation
66  *
67  * BlockHound works by switching a native call by a class generated with ByteBuddy, which, if the blocking
68  * call is allowed in this context, in turn calls the real native call that is now available under a
69  * different name.
70  *
71  * The traces thus undergo the following two changes when the execution is instrumented:
72  *   - The original native call is replaced with a non-native one with the same FQN, and
73  *   - An additional native call is placed on top of the stack, with the original name that also has
74  *     `$$BlockHound$$_` prepended at the last component.
75  */
cleanBlockHoundTracesnull76 private fun cleanBlockHoundTraces(frames: List<String>): List<String> {
77     var result = mutableListOf<String>()
78     val blockHoundSubstr = "\$\$BlockHound\$\$_"
79     var i = 0
80     while (i < frames.size) {
81         result.add(frames[i].replace(blockHoundSubstr, ""))
82         if (frames[i].contains(blockHoundSubstr)) {
83             i += 1
84         }
85         i += 1
86     }
87     return result
88 }
89 
verifyDumpnull90 public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) {
91     val baos = ByteArrayOutputStream()
92     DebugProbes.dumpCoroutines(PrintStream(baos))
93     val trace = baos.toString().split("\n\n")
94     if (traces.isEmpty()) {
95         val filtered = trace.filter { ignoredCoroutine == null || !it.contains(ignoredCoroutine) }
96         assertEquals(1, filtered.count())
97         assertTrue(filtered[0].startsWith("Coroutines dump"))
98         return
99     }
100     // Drop "Coroutine dump" line
101     trace.withIndex().drop(1).forEach { (index, value) ->
102         if (ignoredCoroutine != null && value.contains(ignoredCoroutine)) {
103             return@forEach
104         }
105 
106         val expected = traces[index - 1].applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2)
107         val actual = value.applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2)
108         assertEquals(expected.size, actual.size, "Creation stacktrace should be part of the expected input")
109 
110         expected.withIndex().forEach { (index, trace) ->
111             val actualTrace = actual[index].trimStackTrace().sanitizeAddresses()
112             val expectedTrace = trace.trimStackTrace().sanitizeAddresses()
113             val actualLines = cleanBlockHoundTraces(actualTrace.split("\n"))
114             val expectedLines = expectedTrace.split("\n")
115             for (i in expectedLines.indices) {
116                 assertEquals(expectedLines[i], actualLines[i])
117             }
118         }
119     }
120 }
121 
trimPackagenull122 public fun String.trimPackage() = replace("kotlinx.coroutines.debug.", "")
123 
124 public fun verifyPartialDump(createdCoroutinesCount: Int, vararg frames: String) {
125     val baos = ByteArrayOutputStream()
126     DebugProbes.dumpCoroutines(PrintStream(baos))
127     val dump = baos.toString()
128     val trace = dump.split("\n\n")
129     val matches = frames.all { frame ->
130         trace.any { tr -> tr.contains(frame) }
131     }
132 
133     assertEquals(createdCoroutinesCount, DebugProbes.dumpCoroutinesInfo().size)
134     assertTrue(matches)
135 }
136 
sanitizeAddressesnull137 private fun String.sanitizeAddresses(): String {
138     val index = indexOf("coroutine\"")
139     val next = indexOf(',', index)
140     if (index == -1 || next == -1) return this
141     return substring(0, index) + substring(next, length)
142 }
143