/* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.debug import java.io.* import kotlin.test.* public fun String.trimStackTrace(): String = trimIndent() .replace(Regex(":[0-9]+"), "") .replace(Regex("#[0-9]+"), "") .replace(Regex("(?<=\tat )[^\n]*/"), "") .replace(Regex("\t"), "") .replace("sun.misc.Unsafe.", "jdk.internal.misc.Unsafe.") // JDK8->JDK11 .applyBackspace() public fun String.applyBackspace(): String { val array = toCharArray() val stack = CharArray(array.size) var stackSize = -1 for (c in array) { if (c != '\b') { stack[++stackSize] = c } else { --stackSize } } return String(stack, 0, stackSize + 1) } public fun verifyStackTrace(e: Throwable, traces: List) { val stacktrace = toStackTrace(e) val trimmedStackTrace = stacktrace.trimStackTrace() traces.forEach { assertTrue( trimmedStackTrace.contains(it.trimStackTrace()), "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace" ) } val causes = stacktrace.count("Caused by") assertNotEquals(0, causes) assertEquals(causes, traces.map { it.count("Caused by") }.sum()) } public fun toStackTrace(t: Throwable): String { val sw = StringWriter() t.printStackTrace(PrintWriter(sw)) return sw.toString() } public fun String.count(substring: String): Int = split(substring).size - 1 public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null, finally: () -> Unit) { try { verifyDump(*traces, ignoredCoroutine = ignoredCoroutine) } finally { finally() } } /** Clean the stacktraces from artifacts of BlockHound instrumentation * * BlockHound works by switching a native call by a class generated with ByteBuddy, which, if the blocking * call is allowed in this context, in turn calls the real native call that is now available under a * different name. * * The traces thus undergo the following two changes when the execution is instrumented: * - The original native call is replaced with a non-native one with the same FQN, and * - An additional native call is placed on top of the stack, with the original name that also has * `$$BlockHound$$_` prepended at the last component. */ private fun cleanBlockHoundTraces(frames: List): List { var result = mutableListOf() val blockHoundSubstr = "\$\$BlockHound\$\$_" var i = 0 while (i < frames.size) { result.add(frames[i].replace(blockHoundSubstr, "")) if (frames[i].contains(blockHoundSubstr)) { i += 1 } i += 1 } return result } public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) { val baos = ByteArrayOutputStream() DebugProbes.dumpCoroutines(PrintStream(baos)) val wholeDump = baos.toString() val trace = wholeDump.split("\n\n") if (traces.isEmpty()) { val filtered = trace.filter { ignoredCoroutine == null || !it.contains(ignoredCoroutine) } assertEquals(1, filtered.count()) assertTrue(filtered[0].startsWith("Coroutines dump")) return } // Drop "Coroutine dump" line trace.withIndex().drop(1).forEach { (index, value) -> if (ignoredCoroutine != null && value.contains(ignoredCoroutine)) { return@forEach } val expected = traces[index - 1].applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2) val actual = value.applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2) assertEquals(expected.size, actual.size, "Creation stacktrace should be part of the expected input. Whole dump:\n$wholeDump") expected.withIndex().forEach { (index, trace) -> val actualTrace = actual[index].trimStackTrace().sanitizeAddresses() val expectedTrace = trace.trimStackTrace().sanitizeAddresses() val actualLines = cleanBlockHoundTraces(actualTrace.split("\n")) val expectedLines = expectedTrace.split("\n") for (i in expectedLines.indices) { assertEquals(expectedLines[i], actualLines[i], "Whole dump:\n$wholeDump") } } } } public fun String.trimPackage() = replace("kotlinx.coroutines.debug.", "") public fun verifyPartialDump(createdCoroutinesCount: Int, vararg frames: String) { val baos = ByteArrayOutputStream() DebugProbes.dumpCoroutines(PrintStream(baos)) val dump = baos.toString() val trace = dump.split("\n\n") val matches = frames.all { frame -> trace.any { tr -> tr.contains(frame) } } assertEquals(createdCoroutinesCount, DebugProbes.dumpCoroutinesInfo().size) assertTrue(matches) } private fun String.sanitizeAddresses(): String { val index = indexOf("coroutine\"") val next = indexOf(',', index) if (index == -1 || next == -1) return this return substring(0, index) + substring(next, length) }