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