[JS] Re-run stepping tests if they fail because of a Node crash
KT-54283
This commit is contained in:
committed by
Space Team
parent
53adf097c2
commit
932e1a3c1d
@@ -4,11 +4,11 @@
|
||||
*/
|
||||
package org.jetbrains.kotlin.js.test.debugger
|
||||
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -34,28 +34,16 @@ class NodeJsInspectorClient(val scriptPath: String, val args: List<String>) {
|
||||
*/
|
||||
fun <T> run(block: suspend NodeJsInspectorClientContext.() -> T): T = runBlocking {
|
||||
val context = NodeJsInspectorClientContextImpl(this@NodeJsInspectorClient)
|
||||
|
||||
try {
|
||||
runWithContext(context, block)
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
} catch (e: Throwable) {
|
||||
val nodeExitCode = try {
|
||||
context.nodeProcess.exitValue()
|
||||
} catch (_: IllegalThreadStateException) {
|
||||
throw e
|
||||
}
|
||||
throw IllegalStateException(buildString {
|
||||
append("Node process exited with exit code ")
|
||||
append(nodeExitCode)
|
||||
when (nodeExitCode) {
|
||||
134 -> {
|
||||
append(" (out of memory). Consider increasing ")
|
||||
append((::V8_MAX_OLD_SPACE_SIZE_MB).name)
|
||||
}
|
||||
139 -> {
|
||||
append(" (segmentation fault). Probably a bug in Node (see https://github.com/nodejs/node/issues/45410)")
|
||||
}
|
||||
}
|
||||
append('.')
|
||||
}, e)
|
||||
throw NodeExitedException(nodeExitCode, e)
|
||||
} finally {
|
||||
context.release()
|
||||
}
|
||||
@@ -281,3 +269,27 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) :
|
||||
|
||||
private fun ProcessBuilder.joinedCommand(): String =
|
||||
command().joinToString(" ") { "\"${it.replace("\"", "\\\"")}\"" }
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "CanBeParameter")
|
||||
internal class NodeExitedException(val exitCode: Int, cause: Throwable? = null) : IllegalStateException(createMessage(exitCode), cause) {
|
||||
|
||||
companion object {
|
||||
private fun createMessage(exitCode: Int) = buildString {
|
||||
append("Node process exited with exit code ")
|
||||
append(exitCode)
|
||||
when {
|
||||
!SystemInfo.isWindows && exitCode == 134 -> {
|
||||
append(" (out of memory). Consider increasing ")
|
||||
append((::V8_MAX_OLD_SPACE_SIZE_MB).name)
|
||||
}
|
||||
!SystemInfo.isWindows && exitCode == 139 -> {
|
||||
append(" (segmentation fault). Probably a bug in Node (see https://github.com/nodejs/node/issues/45410)")
|
||||
}
|
||||
SystemInfo.isWindows && exitCode.toUInt() == 0xC0000005U -> {
|
||||
append(" (access violation). Probably a bug in Node (see https://github.com/nodejs/node/issues/45410)")
|
||||
}
|
||||
}
|
||||
append('.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import org.jetbrains.kotlin.test.utils.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* This class is an analogue of the [DebugRunner][org.jetbrains.kotlin.test.backend.handlers.DebugRunner] from JVM stepping tests.
|
||||
@@ -46,6 +48,9 @@ import java.net.URISyntaxException
|
||||
*
|
||||
*/
|
||||
class JsDebugRunner(testServices: TestServices, private val localVariables: Boolean) : AbstractJsArtifactsCollector(testServices) {
|
||||
|
||||
private val logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
override fun processAfterAllModules(someAssertionWasFailed: Boolean) {
|
||||
if (someAssertionWasFailed) return
|
||||
|
||||
@@ -66,7 +71,20 @@ class JsDebugRunner(testServices: TestServices, private val localVariables: Bool
|
||||
is SourceMapError -> error(parseResult.message)
|
||||
}
|
||||
|
||||
runGeneratedCode(jsFilePath, sourceMap, mainModule)
|
||||
val numberOfAttempts = 5
|
||||
retry(
|
||||
numberOfAttempts,
|
||||
action = { runGeneratedCode(jsFilePath, sourceMap, mainModule) },
|
||||
predicate = { attempt, e ->
|
||||
when (e) {
|
||||
is NodeExitedException -> {
|
||||
logger.log(Level.WARNING, "Node.js abruptly exited. Attempt $attempt out of $numberOfAttempts failed.", e)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun runGeneratedCode(
|
||||
@@ -364,3 +382,21 @@ private class ValueDescription(val isNull: Boolean, val isReferenceType: Boolean
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries [action] the specified number of [times]. If [action] throws an exception, calls [predicate] to determine if
|
||||
* another run should be attempted. If [predicate] returns `false`, rethrows the exception.
|
||||
*
|
||||
* If after the last attempt results in an exception, rethrows that exception without calling [predicate].
|
||||
*/
|
||||
internal inline fun <T> retry(times: Int, action: (Int) -> T, predicate: (Int, Throwable) -> Boolean): T {
|
||||
if (times < 1) throw IllegalArgumentException("'times' argument must be at least 1")
|
||||
for (i in 1..times) {
|
||||
try {
|
||||
return action(i)
|
||||
} catch (e: Throwable) {
|
||||
if (i == times || !predicate(i, e)) throw e
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("unreachable")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user