Omit common stack frames in JS full stack trace
#KT-37603
This commit is contained in:
@@ -15,7 +15,7 @@ package kotlin
|
|||||||
* - the detailed description of each throwable in the [Throwable.cause] chain.
|
* - the detailed description of each throwable in the [Throwable.cause] chain.
|
||||||
*/
|
*/
|
||||||
@SinceKotlin("1.4")
|
@SinceKotlin("1.4")
|
||||||
public actual fun Throwable.toStringWithTrace(): String = buildString { dumpStackTraceTo("", "", this, arrayOf()) }
|
public actual fun Throwable.toStringWithTrace(): String = ExceptionTraceBuilder().buildFor(this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the specified exception to the list of exceptions that were
|
* Adds the specified exception to the list of exceptions that were
|
||||||
@@ -43,49 +43,87 @@ public actual val Throwable.suppressedExceptions: List<Throwable>
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun Array<Throwable>.hasSeen(exception: Throwable): Boolean = any { it === exception }
|
private class ExceptionTraceBuilder {
|
||||||
|
private val target = StringBuilder()
|
||||||
|
private val visited = arrayOf<Throwable>()
|
||||||
|
private var topStack: String = ""
|
||||||
|
private var topStackStart: Int = 0
|
||||||
|
|
||||||
private fun Throwable.dumpStackTraceTo(indent: String, qualifier: String, target: StringBuilder, visited: Array<Throwable>) {
|
fun buildFor(exception: Throwable): String {
|
||||||
this.dumpSelfTrace(indent, qualifier, target, visited) || return
|
exception.dumpFullTrace("", "")
|
||||||
|
return target.toString()
|
||||||
var cause = this.cause
|
|
||||||
while (cause != null) {
|
|
||||||
// TODO: should skip common stack frames
|
|
||||||
cause.dumpSelfTrace(indent, "Caused by: ", target, visited)
|
|
||||||
cause = cause.cause
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun Throwable.dumpSelfTrace(indent: String, qualifier: String, target: StringBuilder, visited: Array<Throwable>): Boolean {
|
private fun hasSeen(exception: Throwable): Boolean = visited.any { it === exception }
|
||||||
target.append(indent).append(qualifier)
|
|
||||||
if (visited.hasSeen(this)) {
|
private fun Throwable.dumpFullTrace(indent: String, qualifier: String) {
|
||||||
target.append("[CIRCULAR REFERENCE, SEE ABOVE: ").append(this).append("]\n")
|
this.dumpSelfTrace(indent, qualifier) || return
|
||||||
return false
|
|
||||||
|
var cause = this.cause
|
||||||
|
while (cause != null) {
|
||||||
|
cause.dumpSelfTrace(indent, "Caused by: ") || return
|
||||||
|
cause = cause.cause
|
||||||
|
}
|
||||||
}
|
}
|
||||||
visited.asDynamic().push(this)
|
|
||||||
|
|
||||||
val stack = this.asDynamic().stack as String?
|
private fun Throwable.dumpSelfTrace(indent: String, qualifier: String): Boolean {
|
||||||
if (stack != null) {
|
target.append(indent).append(qualifier)
|
||||||
if (indent.isNotEmpty()) {
|
if (hasSeen(this)) {
|
||||||
// indent stack, but avoid indenting exception message lines
|
target.append("[CIRCULAR REFERENCE, SEE ABOVE: ").append(this).append("]\n")
|
||||||
val messageLines = 1 + (message?.count { c -> c == '\n' } ?: 0)
|
return false
|
||||||
stack.lineSequence().forEachIndexed { index: Int, line: String ->
|
}
|
||||||
if (index >= messageLines) target.append(indent)
|
visited.asDynamic().push(this)
|
||||||
target.append(line).append("\n")
|
|
||||||
|
var stack = this.asDynamic().stack as String?
|
||||||
|
if (stack != null) {
|
||||||
|
if (topStack.isEmpty()) {
|
||||||
|
topStack = stack
|
||||||
|
topStackStart = this.toString().length
|
||||||
|
} else {
|
||||||
|
stack = dropCommonFrames(stack, this.toString().length)
|
||||||
|
}
|
||||||
|
if (indent.isNotEmpty()) {
|
||||||
|
// indent stack, but avoid indenting exception message lines
|
||||||
|
val messageLines = 1 + (message?.count { c -> c == '\n' } ?: 0)
|
||||||
|
stack.lineSequence().forEachIndexed { index: Int, line: String ->
|
||||||
|
if (index >= messageLines) target.append(indent)
|
||||||
|
target.append(line).append("\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target.append(stack).append("\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
target.append(stack).append("\n")
|
target.append(this.toString()).append("\n")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
target.append(this.toString()).append("\n")
|
val suppressed = suppressedExceptions
|
||||||
|
if (suppressed.isNotEmpty()) {
|
||||||
|
val suppressedIndent = indent + " "
|
||||||
|
for (s in suppressed) {
|
||||||
|
s.dumpFullTrace(suppressedIndent, "Suppressed: ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
val suppressed = suppressedExceptions
|
private fun dropCommonFrames(stack: String, stackStart: Int): String {
|
||||||
if (suppressed.isNotEmpty()) {
|
var commonFrames: Int = 0
|
||||||
val suppressedIndent = indent + '\t'
|
var lastBreak: Int = 0
|
||||||
for (s in suppressed) {
|
var preLastBreak: Int = 0
|
||||||
s.dumpStackTraceTo(suppressedIndent, "Suppressed: ", target, visited)
|
for (pos in 0 until minOf(topStack.length - topStackStart, stack.length - stackStart)) {
|
||||||
|
val c = stack[stack.lastIndex - pos]
|
||||||
|
if (c != topStack[topStack.lastIndex - pos]) break
|
||||||
|
if (c == '\n') {
|
||||||
|
commonFrames += 1
|
||||||
|
preLastBreak = lastBreak
|
||||||
|
lastBreak = pos
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (commonFrames <= 1) return stack
|
||||||
|
while (preLastBreak > 0 && stack[stack.lastIndex - (preLastBreak - 1)] == ' ')
|
||||||
|
preLastBreak -= 1
|
||||||
|
|
||||||
|
// leave 1 common frame to ease matching with the top exception stack
|
||||||
|
return stack.dropLast(preLastBreak) + "... and ${commonFrames - 1} more common stack frames skipped"
|
||||||
}
|
}
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
@@ -97,7 +97,8 @@ class ExceptionTest {
|
|||||||
try {
|
try {
|
||||||
root()
|
root()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.addSuppressed(suppressedError(1))
|
for (id in 0..1)
|
||||||
|
e.addSuppressed(suppressedError(id))
|
||||||
throw RuntimeException("Induced", e)
|
throw RuntimeException("Induced", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,8 +124,9 @@ class ExceptionTest {
|
|||||||
if (supportsSuppressedExceptions) {
|
if (supportsSuppressedExceptions) {
|
||||||
val topLevelSuppressed = e.suppressedExceptions.single()
|
val topLevelSuppressed = e.suppressedExceptions.single()
|
||||||
assertInTrace(topLevelSuppressed)
|
assertInTrace(topLevelSuppressed)
|
||||||
val causeSuppressed = cause.suppressedExceptions.single()
|
cause.suppressedExceptions.forEach {
|
||||||
assertInTrace(causeSuppressed)
|
assertInTrace(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail(topLevelTrace) // to dump the entire trace
|
// fail(topLevelTrace) // to dump the entire trace
|
||||||
@@ -136,6 +138,7 @@ class ExceptionTest {
|
|||||||
|
|
||||||
// Testing an exception of the following structure
|
// Testing an exception of the following structure
|
||||||
// e1
|
// e1
|
||||||
|
// -- suppressed: e0 (same stack as e1)
|
||||||
// -- suppressed: e3
|
// -- suppressed: e3
|
||||||
// -- suppressed: e1
|
// -- suppressed: e1
|
||||||
// Caused by: e2
|
// Caused by: e2
|
||||||
@@ -144,15 +147,22 @@ class ExceptionTest {
|
|||||||
|
|
||||||
val e3 = Exception("e3")
|
val e3 = Exception("e3")
|
||||||
val e2 = Error("e2", e3)
|
val e2 = Error("e2", e3)
|
||||||
val e1 = RuntimeException("e1", e2)
|
val (e1, e0) = listOf("e1", "e0").map { msg -> RuntimeException(msg, e2.takeIf { msg == "e1" }) }
|
||||||
|
e1.addSuppressed(e0)
|
||||||
e1.addSuppressed(e3)
|
e1.addSuppressed(e3)
|
||||||
e3.addSuppressed(e1)
|
e3.addSuppressed(e1)
|
||||||
e2.addSuppressed(e1)
|
e2.addSuppressed(e1)
|
||||||
|
|
||||||
val topLevelTrace = e1.toStringWithTrace()
|
val topLevelTrace = e1.toStringWithTrace()
|
||||||
assertEquals(3, Regex.fromLiteral(e1.toString()).findAll(topLevelTrace).count(), topLevelTrace)
|
fun assertAppearsInTrace(value: Any, count: Int) {
|
||||||
assertEquals(1, Regex.fromLiteral(e2.toString()).findAll(topLevelTrace).count(), topLevelTrace)
|
if (Regex.fromLiteral(value.toString()).findAll(topLevelTrace).count() != count) {
|
||||||
assertEquals(2, Regex.fromLiteral(e3.toString()).findAll(topLevelTrace).count(), topLevelTrace)
|
fail("Expected to find $value $count times in $topLevelTrace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertAppearsInTrace(e1, 3)
|
||||||
|
assertAppearsInTrace(e0, 1)
|
||||||
|
assertAppearsInTrace(e2, 1)
|
||||||
|
assertAppearsInTrace(e3, 2)
|
||||||
// fail(topLevelTrace) // to dump the entire trace
|
// fail(topLevelTrace) // to dump the entire trace
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user