diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/NodeJsInspectorClient.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/NodeJsInspectorClient.kt index 962d3f36f7a..ce4ec589f5b 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/NodeJsInspectorClient.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/NodeJsInspectorClient.kt @@ -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) { */ fun 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('.') + } + } +} diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/handlers/JsDebugRunner.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/handlers/JsDebugRunner.kt index f5fd96da839..019f12d2cbb 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/handlers/JsDebugRunner.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/handlers/JsDebugRunner.kt @@ -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 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") +}