Implement proper script runtime exception rendering with tests

#KT-42335 fixed
This commit is contained in:
Ilya Chernikov
2020-10-01 13:15:05 +02:00
parent d5ad424e8f
commit db23460fd5
7 changed files with 165 additions and 20 deletions
@@ -87,6 +87,22 @@ class ReplTest : TestCase() {
)
}
@Test
fun testEvalWithExceptionWithCause() {
checkEvaluateInRepl(
sequenceOf(
"""
try {
throw Exception("Error!")
} catch (e: Exception) {
throw Exception("Oh no", e)
}
""".trimIndent()
),
sequenceOf(Exception("Oh no", Exception("Error!")))
)
}
@Test
fun testEvalWithErrorWithLocation() {
checkEvaluateInReplDiags(
@@ -240,6 +256,7 @@ class ReplTest : TestCase() {
is ResultValue.Error -> Assert.assertTrue(
"#$index: Expected $expectedVal, got Error: ${actualVal.error}",
expectedVal is Throwable && expectedVal.message == actualVal.error.message
&& expectedVal.cause?.message == actualVal.error.cause?.message
)
is ResultValue.NotEvaluated -> Assert.assertEquals(
"#$index: Expected $expectedVal, got NotEvaluated",
@@ -0,0 +1,44 @@
/*
* 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.
*/
package kotlin.script.experimental.jvm.util
import java.io.PrintStream
import kotlin.script.experimental.api.ResultValue
fun ResultValue.Error.renderError(stream: PrintStream) {
var trace = error.stackTrace
val wrappingTrace = wrappingException?.stackTrace
if (wrappingException == null || trace.size < wrappingTrace!!.size) {
error.printStackTrace(stream)
} else {
// subtracting wrapping message stacktrace from error stacktrace to show only user-specific part of it
fun PrintStream.printTrace(stackTrace: Array<StackTraceElement>, dropLastFrames: Int) {
for (element in stackTrace.dropLast(dropLastFrames)) {
println("\tat $element")
}
}
stream.println(error)
stream.printTrace(trace, wrappingTrace.size)
var current: Throwable? = error.cause
var wrapping = error
val cyclesDetection = hashSetOf(wrapping)
while (current != null && cyclesDetection.add(current)) {
trace = current.stackTrace
val sameFramesCount =
trace.asList().asReversed().asSequence()
.zip(wrapping.stackTrace.asList().asReversed().asSequence())
.takeWhile { it.first == it.second }
.count()
stream.println("Caused by: $current")
stream.printTrace(trace, sameFramesCount)
wrapping = current
current = current.cause
}
}
}
@@ -19,10 +19,11 @@ import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.scripting.configuration.ScriptingConfigurationKeys
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinitionProvider
import java.io.File
import java.io.PrintStream
import java.util.*
import kotlin.script.experimental.api.*
import kotlin.script.experimental.host.StringScriptSource
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.util.renderError
abstract class AbstractScriptEvaluationExtension : ScriptEvaluationExtension {
@@ -70,13 +71,19 @@ abstract class AbstractScriptEvaluationExtension : ScriptEvaluationExtension {
val extensionHint =
if (configuration.get(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS)?.let { it.size == 1 && it.first().isDefault } == true) " (.kts)"
else ""
messageCollector.report(CompilerMessageSeverity.ERROR, "$error; Specify path to the script file$extensionHint as the first argument")
messageCollector.report(
CompilerMessageSeverity.ERROR,
"$error; Specify path to the script file$extensionHint as the first argument"
)
return ExitCode.COMPILATION_ERROR
}
script
}
else -> {
messageCollector.report(CompilerMessageSeverity.ERROR, "Illegal set of arguments: either -script or -expression arguments expected at this point")
messageCollector.report(
CompilerMessageSeverity.ERROR,
"Illegal set of arguments: either -script or -expression arguments expected at this point"
)
return ExitCode.COMPILATION_ERROR
}
}
@@ -150,19 +157,3 @@ fun ScriptDiagnostic.Severity.toCompilerMessageSeverity(): CompilerMessageSeveri
ScriptDiagnostic.Severity.DEBUG -> CompilerMessageSeverity.LOGGING
}
private fun ResultValue.Error.renderError(stream: PrintStream) {
val fullTrace = error.stackTrace
if (wrappingException == null || fullTrace.size < wrappingException!!.stackTrace.size) {
error.printStackTrace(stream)
} else {
// subtracting wrapping message stacktrace from error stacktrace to show only user-specific part of it
// TODO: consider more reliable logic, e.g. comparing traces, fallback to full error printing in case of mismatch
// TODO: write tests
stream.println(error)
val scriptTraceSize = fullTrace.size - wrappingException!!.stackTrace.size
for (i in 0 until scriptTraceSize) {
stream.println("\tat " + fullTrace[i])
}
}
}
@@ -0,0 +1,9 @@
try {
try {
throw Exception("Error!")
} catch (e: Exception) {
throw Exception("Oh no", e)
}
} catch (e: Exception) {
throw Exception("Top", e)
}
@@ -5,7 +5,10 @@
package org.jetbrains.kotlin.scripting.compiler.plugin
import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.jetbrains.kotlin.scripting.compiler.test.linesSplitTrim
import org.junit.Assert
import org.junit.Test
import java.io.File
@@ -84,6 +87,31 @@ class ScriptingWithCliCompilerTest {
)
}
@Test
fun testExceptionWithCause() {
val (_, err, _) = captureOutErrRet {
CLITool.doMainNoExit(
K2JVMCompiler(),
arrayOf(
"-script",
"$TEST_DATA_DIR/integration/exceptionWithCause.kts"
)
)
}
val filteredErr = err.linesSplitTrim().filterNot { it.startsWith("WARN: ") }
Assert.assertEquals(
"""
java.lang.Exception: Top
at ExceptionWithCause.<init>(exceptionWithCause.kts:8)
Caused by: java.lang.Exception: Oh no
at ExceptionWithCause.<init>(exceptionWithCause.kts:5)
Caused by: java.lang.Exception: Error!
at ExceptionWithCause.<init>(exceptionWithCause.kts:3)
""".trimIndent().linesSplitTrim(),
filteredErr
)
}
private fun getMainKtsClassPath(): List<File> {
return listOf(
File("dist/kotlinc/lib/kotlin-main-kts.jar").also {
@@ -0,0 +1,56 @@
import junit.framework.TestCase
import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.ScriptJvmCompilerIsolated
import org.jetbrains.kotlin.scripting.compiler.test.assertEqualsTrimmed
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import kotlin.script.experimental.api.*
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.BasicJvmScriptEvaluator
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
import kotlin.script.experimental.jvm.util.renderError
/*
* 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.
*/
class ScriptEvaluationTest : TestCase() {
fun testExceptionWithCause() {
checkEvaluate(
"""
try {
throw Exception("Error!")
} catch (e: Exception) {
throw Exception("Oh no", e)
}
""".trimIndent().toScriptSource("exceptionWithCause.kts"),
"""
java.lang.Exception: Oh no
at ExceptionWithCause.<init>(exceptionWithCause.kts:4)
Caused by: java.lang.Exception: Error!
at ExceptionWithCause.<init>(exceptionWithCause.kts:2)
""".trimIndent()
)
}
private fun checkEvaluate(script: SourceCode, expectedOutput: String) {
val compilationConfiguration = ScriptCompilationConfiguration()
val compiler = ScriptJvmCompilerIsolated(defaultJvmScriptingHostConfiguration)
val compiled = compiler.compile(script, compilationConfiguration).valueOrThrow()
val evaluationConfiguration = ScriptEvaluationConfiguration()
val evaluator = BasicJvmScriptEvaluator()
val res = runBlocking {
evaluator.invoke(compiled, evaluationConfiguration).valueOrThrow()
}
assert(res.returnValue is ResultValue.Error)
ByteArrayOutputStream().use { os ->
val ps = PrintStream(os)
(res.returnValue as ResultValue.Error).renderError(ps)
ps.flush()
assertEqualsTrimmed(expectedOutput, os.toString())
}
}
}
@@ -53,7 +53,7 @@ internal fun captureOut(body: () -> Unit): String {
return outStream.toString()
}
private fun String.linesSplitTrim() =
internal fun String.linesSplitTrim() =
split('\n', '\r').map(String::trim).filter(String::isNotBlank)
internal fun assertEqualsTrimmed(expected: String, actual: String) =