[JS] Re-run stepping tests if they fail because of a Node crash

KT-54283
This commit is contained in:
Sergej Jaskiewicz
2022-11-16 15:44:32 +01:00
committed by Space Team
parent 53adf097c2
commit 932e1a3c1d
2 changed files with 65 additions and 17 deletions
@@ -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")
}