diff --git a/libraries/stdlib/common/src/kotlin/ExceptionsH.kt b/libraries/stdlib/common/src/kotlin/ExceptionsH.kt index 6551c443e2a..9132d5b622e 100644 --- a/libraries/stdlib/common/src/kotlin/ExceptionsH.kt +++ b/libraries/stdlib/common/src/kotlin/ExceptionsH.kt @@ -118,6 +118,17 @@ internal class KotlinNothingValueException : RuntimeException { } +/** + * Returns the detailed description of this throwable with its stack trace. + * + * The detailed description includes: + * - the short description (see [Throwable.toString]) of this throwable; + * - the complete stack trace; + * - detailed descriptions of the exceptions that were [suppressed][suppressedExceptions] in order to deliver this exception; + * - the detailed description of each throwable in the [Throwable.cause] chain. + */ +@SinceKotlin("1.4") +public expect fun Throwable.toStringWithTrace(): String /** * When supported by the platform, adds the specified exception to the list of exceptions that were * suppressed in order to deliver this exception. diff --git a/libraries/stdlib/js/src/kotlin/throwableExtensions.kt b/libraries/stdlib/js/src/kotlin/throwableExtensions.kt index ddb00d9292f..be95aa85586 100644 --- a/libraries/stdlib/js/src/kotlin/throwableExtensions.kt +++ b/libraries/stdlib/js/src/kotlin/throwableExtensions.kt @@ -5,6 +5,18 @@ package kotlin +/** + * Returns the detailed description of this throwable with its stack trace. + * + * The detailed description includes: + * - the short description (see [Throwable.toString]) of this throwable; + * - the complete stack trace; + * - detailed descriptions of the exceptions that were [suppressed][suppressedExceptions] in order to deliver this exception; + * - the detailed description of each throwable in the [Throwable.cause] chain. + */ +@SinceKotlin("1.4") +public actual fun Throwable.toStringWithTrace(): String = buildString { dumpStackTraceTo("", "", this) } + /** * Adds the specified exception to the list of exceptions that were * suppressed in order to deliver this exception. @@ -29,3 +41,42 @@ public actual val Throwable.suppressedExceptions: List get() { return this.asDynamic()._suppressed?.unsafeCast>() ?: emptyList() } + + +private fun Throwable.dumpStackTraceTo(indent: String, qualifier: String, target: StringBuilder) { + this.dumpSelfTrace(indent, qualifier, target) + + var cause = this.cause + while (cause != null) { + // TODO: should skip common stack frames + cause.dumpSelfTrace(indent, "Caused by: ", target) + cause = cause.cause + } +} + +private fun Throwable.dumpSelfTrace(indent: String, qualifier: String, target: StringBuilder) { + target.append(indent).append(qualifier) + val stack = this.asDynamic().stack as String? + if (stack != null) { + 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 { + target.append(this.toString()).append("\n") + } + + val suppressed = suppressedExceptions + if (suppressed.isNotEmpty()) { + val suppressedIndent = indent + '\t' + for (s in suppressed) { + s.dumpStackTraceTo(suppressedIndent, "Suppressed: ", target) + } + } +} diff --git a/libraries/stdlib/jvm/src/kotlin/util/Exceptions.kt b/libraries/stdlib/jvm/src/kotlin/util/Exceptions.kt index 55cb9394c0e..21fb871312c 100644 --- a/libraries/stdlib/jvm/src/kotlin/util/Exceptions.kt +++ b/libraries/stdlib/jvm/src/kotlin/util/Exceptions.kt @@ -10,24 +10,25 @@ package kotlin import java.io.PrintStream import java.io.PrintWriter +import java.io.StringWriter import kotlin.internal.* /** - * Prints the stack trace of this throwable to the standard output. + * Prints the [detailed description][Throwable.toStringWithTrace] of this throwable to the standard error output. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") // to be used when no member available @kotlin.internal.InlineOnly public inline fun Throwable.printStackTrace(): Unit = (this as java.lang.Throwable).printStackTrace() /** - * Prints the stack trace of this throwable to the specified [writer]. + * Prints the [detailed description][Throwable.toStringWithTrace] of this throwable to the specified [writer]. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") // to be used when no member available @kotlin.internal.InlineOnly public inline fun Throwable.printStackTrace(writer: PrintWriter): Unit = (this as java.lang.Throwable).printStackTrace(writer) /** - * Prints the stack trace of this throwable to the specified [stream]. + * Prints the [detailed description][Throwable.toStringWithTrace] of this throwable to the specified [stream]. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") // to be used when no member available @kotlin.internal.InlineOnly @@ -41,6 +42,24 @@ public inline fun Throwable.printStackTrace(stream: PrintStream): Unit = (this a public val Throwable.stackTrace: Array get() = (this as java.lang.Throwable).stackTrace!! +/** + * Returns the detailed description of this throwable with its stack trace. + * + * The detailed description includes: + * - the short description (see [Throwable.toString]) of this throwable; + * - the complete stack trace; + * - detailed descriptions of the exceptions that were [suppressed][suppressedExceptions] in order to deliver this exception; + * - the detailed description of each throwable in the [Throwable.cause] chain. + */ +@SinceKotlin("1.4") +public actual fun Throwable.toStringWithTrace(): String { + val sw = StringWriter() + val pw = PrintWriter(sw) + printStackTrace(pw) + pw.flush() + return sw.toString() +} + /** * When supported by the platform, adds the specified exception to the list of exceptions that were * suppressed in order to deliver this exception. diff --git a/libraries/stdlib/jvm/test/ExceptionJVMTest.kt b/libraries/stdlib/jvm/test/ExceptionJVMTest.kt index 445d9176e7c..0ae48e92145 100644 --- a/libraries/stdlib/jvm/test/ExceptionJVMTest.kt +++ b/libraries/stdlib/jvm/test/ExceptionJVMTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -7,6 +7,7 @@ package test.exceptions import kotlin.test.* import test.collections.assertArrayNotSameButEquals +import test.testOnJvm7AndAbove import java.io.PrintWriter import java.io.* @@ -17,11 +18,13 @@ class ExceptionJVMTest { @Test fun printStackTraceOnRuntimeException() { assertPrintStackTrace(RuntimeException("Crikey!")) assertPrintStackTraceStream(RuntimeException("Crikey2")) + assertToStringWithTrace(RuntimeException("ToString")) } @Test fun printStackTraceOnError() { assertPrintStackTrace(Error("Oh dear")) assertPrintStackTraceStream(Error("Oh dear2")) + assertToStringWithTrace(Error("ToString")) } @@ -49,6 +52,11 @@ class ExceptionJVMTest { comparePrintedThrowableResult(t, content) } + fun assertToStringWithTrace(t: Throwable) { + val content = t.toStringWithTrace() + comparePrintedThrowableResult(t, content) + } + private fun comparePrintedThrowableResult(throwable: Throwable, printedThrowable: CharSequence) { val stackTrace = throwable.stackTrace val lines = printedThrowable.lines() @@ -72,4 +80,16 @@ class ExceptionJVMTest { e1.addSuppressed(e2) } + + @Test fun circularCauseStackTrace() { + val e1 = Exception("cause") + val e2 = Error("induced", e1) + e1.initCause(e2) + assertSame(e1, e2.cause) + assertSame(e2, e1.cause) + testOnJvm7AndAbove { + val trace = e2.toStringWithTrace() + assertTrue("CIRCULAR REFERENCE" in trace, trace) + } + } } diff --git a/libraries/stdlib/jvm/test/testUtilsJVM.kt b/libraries/stdlib/jvm/test/testUtilsJVM.kt index 17f17129573..8b5c875efa0 100644 --- a/libraries/stdlib/jvm/test/testUtilsJVM.kt +++ b/libraries/stdlib/jvm/test/testUtilsJVM.kt @@ -26,6 +26,11 @@ internal actual inline fun testOnNonJvm6And7(f: () -> Unit) { f() } } +internal inline fun testOnJvm7AndAbove(f: () -> Unit) { + if (!isJava6) { + f() + } +} public actual fun testOnJvm(action: () -> Unit) = action() public actual fun testOnJs(action: () -> Unit) {} diff --git a/libraries/stdlib/test/ExceptionTest.kt b/libraries/stdlib/test/ExceptionTest.kt index 49424a012f7..9a2f768b37c 100644 --- a/libraries/stdlib/test/ExceptionTest.kt +++ b/libraries/stdlib/test/ExceptionTest.kt @@ -87,4 +87,47 @@ class ExceptionTest { } } + @Test + fun exceptionDetailedTrace() { + fun root(): Nothing = throw IllegalStateException("Root cause\nDetails: root") + + fun suppressedError(id: Int): Throwable = UnsupportedOperationException("Side error\nId: $id") + + fun induced(): Nothing { + try { + root() + } catch (e: Throwable) { + e.addSuppressed(suppressedError(1)) + throw RuntimeException("Induced", e) + } + } + + val e = try { + induced() + } catch (e: Throwable) { + e.apply { addSuppressed(suppressedError(2)) } + } + + val topLevelTrace = e.toStringWithTrace() + fun assertInTrace(value: Any) { + if (value.toString() !in topLevelTrace) { + fail("Expected top level trace: $topLevelTrace\n\nto contain: $value") + } + } + + assertInTrace(e) + + val cause = assertNotNull(e.cause, "Should have cause") + assertInTrace(cause) + + if (supportsSuppressedExceptions) { + val topLevelSuppressed = e.suppressedExceptions.single() + assertInTrace(topLevelSuppressed) + val causeSuppressed = cause.suppressedExceptions.single() + assertInTrace(causeSuppressed) + } + +// fail(topLevelTrace) // to dump the entire trace + } + } \ No newline at end of file diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index 8feebbb9a6f..f01de1ccd92 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -22,6 +22,7 @@ public final class kotlin/ExceptionsKt { public static final fun addSuppressed (Ljava/lang/Throwable;Ljava/lang/Throwable;)V public static final fun getStackTrace (Ljava/lang/Throwable;)[Ljava/lang/StackTraceElement; public static final fun getSuppressedExceptions (Ljava/lang/Throwable;)Ljava/util/List; + public static final fun toStringWithTrace (Ljava/lang/Throwable;)Ljava/lang/String; } public abstract interface annotation class kotlin/Experimental : java/lang/annotation/Annotation {