[JS IR] Use a special executor for stepping tests

This will hopefully fix flakiness for some tests
This commit is contained in:
Sergej Jaskiewicz
2022-08-26 19:44:21 +02:00
committed by Space
parent 6ebbe12d44
commit 17f22c7204
4 changed files with 119 additions and 42 deletions
+26 -1
View File
@@ -1,4 +1,4 @@
// IGNORE_BACKEND: JS_IR
// FILE: test.kt // FILE: test.kt
fun foo() { fun foo() {
@@ -93,3 +93,28 @@ fun box() {
// test.kt:39 box // test.kt:39 box
// EXPECTATIONS JS_IR // EXPECTATIONS JS_IR
// test.kt:34 box
// test.kt:6 foo
// test.kt:26 mightThrow
// test.kt:14 foo
// test.kt:30 mightThrow2
// test.kt:13 foo
// test.kt:35 box
// test.kt:36 box
// test.kt:6 foo
// test.kt:26 mightThrow
// test.kt:14 foo
// test.kt:30 mightThrow2
// test.kt:30 mightThrow2
// test.kt:16 foo
// test.kt:13 foo
// test.kt:37 box
// test.kt:38 box
// test.kt:6 foo
// test.kt:26 mightThrow
// test.kt:26 mightThrow
// test.kt:14 foo
// test.kt:30 mightThrow2
// test.kt:30 mightThrow2
// test.kt:16 foo
// test.kt:13 foo
@@ -13,10 +13,7 @@ import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.kotlin.utils.addToStdlib.cast import org.jetbrains.kotlin.utils.addToStdlib.cast
import java.util.logging.ConsoleHandler
import java.util.logging.Level
import java.util.logging.Logger import java.util.logging.Logger
import java.util.logging.MemoryHandler
import kotlin.coroutines.* import kotlin.coroutines.*
/** /**
@@ -59,21 +56,32 @@ class NodeJsInspectorClient(val scriptPath: String, val args: List<String>) {
} }
}) })
context.listenForMessages { message -> try {
when (val response = decodeCDPResponse(message) { context.messageContinuations[it]!!.first }) { context.listenForMessages { message ->
is CDPResponse.Event -> onDebuggerEventCallback?.invoke(response.event) when (val response = decodeCDPResponse(message) { context.messageContinuations[it]!!.encodingInfo }) {
is CDPResponse.MethodInvocationResult -> context.messageContinuations.remove(response.id)!!.second.resume(response.result) is CDPResponse.Event -> onDebuggerEventCallback?.invoke(response.event)
is CDPResponse.Error -> context.messageContinuations[response.id]!!.second.resumeWithException( is CDPResponse.MethodInvocationResult -> context.messageContinuations.remove(response.id)!!.continuation.resume(response.result)
IllegalStateException("error ${response.error.code}" + (response.error.message?.let { ": $it" } ?: "")) is CDPResponse.Error -> context.messageContinuations[response.id]!!.let { (_, continuation, stackTrace) ->
) continuation.resumeWithException(
} IllegalStateException("error ${response.error.code}" + (response.error.message?.let { ": $it" } ?: "")).apply {
context.waitingOnPredicate?.let { (predicate, continuation) -> this.stackTrace = stackTrace
if (predicate()) { }
context.waitingOnPredicate = null )
continuation.resume(Unit) }
} }
context.waitingOnPredicate?.let { (predicate, continuation, _) ->
if (predicate()) {
context.waitingOnPredicate = null
continuation.resume(Unit)
}
}
blockResult != null
} }
blockResult != null } catch (e: Exception) {
val callerStackTrace = context.messageContinuations.values.singleOrNull()?.stackTrace ?: context.waitingOnPredicate?.stackTrace
if (callerStackTrace != null)
e.stackTrace = callerStackTrace
throw e
} }
return blockResult!!.getOrThrow() return blockResult!!.getOrThrow()
@@ -94,16 +102,7 @@ private const val NODE_WS_DEBUG_URL_PREFIX = "Debugger listening on ws://"
*/ */
private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) : NodeJsInspectorClientContext, CDPRequestEvaluator { private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) : NodeJsInspectorClientContext, CDPRequestEvaluator {
private val consoleLoggingHandler = ConsoleHandler().apply { private val logger = Logger.getLogger(this::class.java.name)
level = Level.FINER
}
private val memoryLoggingHandler = MemoryHandler(consoleLoggingHandler, 5000, Level.WARNING)
private val logger = Logger.getLogger(this::class.java.name).apply {
level = Level.FINER
addHandler(memoryLoggingHandler)
}
private val nodeProcess = ProcessBuilder( private val nodeProcess = ProcessBuilder(
System.getProperty("javascript.engine.path.NodeJs"), System.getProperty("javascript.engine.path.NodeJs"),
@@ -130,16 +129,31 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) :
private val webSocketClient = HttpClient(CIO) { private val webSocketClient = HttpClient(CIO) {
install(WebSockets) install(WebSockets)
engine {
requestTimeout = 0
}
} }
private var webSocketSession: DefaultClientWebSocketSession? = null private var webSocketSession: DefaultClientWebSocketSession? = null
val messageContinuations = mutableMapOf<Int, Pair<CDPMethodCallEncodingInfo, Continuation<CDPMethodInvocationResult>>>() data class MessageContinuation(
val encodingInfo: CDPMethodCallEncodingInfo,
val continuation: Continuation<CDPMethodInvocationResult>,
val stackTrace: Array<StackTraceElement>
)
val messageContinuations = mutableMapOf<Int, MessageContinuation>()
data class WaitingOnPredicate(
val predicate: () -> Boolean,
val continuation: Continuation<Unit>,
val stackTrace: Array<StackTraceElement>,
)
/** /**
* See [waitForConditionToBecomeTrue]. * See [waitForConditionToBecomeTrue].
*/ */
var waitingOnPredicate: Pair<(() -> Boolean), Continuation<Unit>>? = null var waitingOnPredicate: WaitingOnPredicate? = null
private var nextMessageId = 0 private var nextMessageId = 0
@@ -165,14 +179,9 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) :
suspend fun listenForMessages(receiveMessage: (String) -> Boolean) { suspend fun listenForMessages(receiveMessage: (String) -> Boolean) {
val session = webSocketSession ?: error("Session closed") val session = webSocketSession ?: error("Session closed")
do { do {
val message = try { val message = when (val frame = session.incoming.receive()) {
when (val frame = session.incoming.receive()) { is Frame.Text -> frame.readText()
is Frame.Text -> frame.readText() else -> error("Unexpected frame kind: $frame")
else -> error("Unexpected frame kind: $frame")
}
} catch (e: Exception) {
logger.log(Level.SEVERE, "Could not receive message", e)
throw e
} }
logger.finer { logger.finer {
"Received message:\n${prettyPrintJson(message)}" "Received message:\n${prettyPrintJson(message)}"
@@ -186,9 +195,13 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) :
override suspend fun waitForConditionToBecomeTrue(predicate: () -> Boolean) { override suspend fun waitForConditionToBecomeTrue(predicate: () -> Boolean) {
if (predicate()) return if (predicate()) return
// Save the stack trace to show it later. If the condition never becomes true due to an exception,
// this stack trace will be shown instead of some obscure coroutine-related one, which will make debugging easier.
val stacktrace = Thread.currentThread().stackTrace
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
require(waitingOnPredicate == null) { "already waiting!" } require(waitingOnPredicate == null) { "already waiting!" }
waitingOnPredicate = predicate to continuation waitingOnPredicate = WaitingOnPredicate(predicate, continuation, stacktrace)
} }
} }
@@ -203,9 +216,14 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) :
@Deprecated("Only for debugging purposes", level = DeprecationLevel.WARNING) @Deprecated("Only for debugging purposes", level = DeprecationLevel.WARNING)
override suspend fun sendPlainTextMessage(methodName: String, paramsJson: String): String { override suspend fun sendPlainTextMessage(methodName: String, paramsJson: String): String {
val messageId = nextMessageId++ val messageId = nextMessageId++
// Save the stack trace to show it later. If we won't be able to receive a response for this message due to an exception,
// this stack trace will be shown instead of some obscure coroutine-related one, which will make debugging easier.
val stacktrace = Thread.currentThread().stackTrace
sendPlainTextMessage("""{"id":$messageId,"method":$methodName,"params":$paramsJson}""") sendPlainTextMessage("""{"id":$messageId,"method":$methodName,"params":$paramsJson}""")
return suspendCoroutine { continuation -> return suspendCoroutine { continuation ->
messageContinuations[messageId] = CDPMethodCallEncodingInfoPlainText to continuation messageContinuations[messageId] = MessageContinuation(CDPMethodCallEncodingInfoPlainText, continuation, stacktrace)
}.cast<CDPMethodInvocationResultPlainText>().string }.cast<CDPMethodInvocationResultPlainText>().string
} }
@@ -213,10 +231,15 @@ private class NodeJsInspectorClientContextImpl(engine: NodeJsInspectorClient) :
encodeMethodCallWithMessageId: (Int) -> Pair<String, CDPMethodCallEncodingInfo> encodeMethodCallWithMessageId: (Int) -> Pair<String, CDPMethodCallEncodingInfo>
): CDPMethodInvocationResult { ): CDPMethodInvocationResult {
val messageId = nextMessageId++ val messageId = nextMessageId++
// Save the stack trace to show it later. If we won't be able to receive a response for this message due to an exception,
// this stack trace will be shown instead of some obscure coroutine-related one, which will make debugging easier.
val stacktrace = Thread.currentThread().stackTrace
val (encodedMessage, encodingInfo) = encodeMethodCallWithMessageId(messageId) val (encodedMessage, encodingInfo) = encodeMethodCallWithMessageId(messageId)
sendPlainTextMessage(encodedMessage) sendPlainTextMessage(encodedMessage)
return suspendCoroutine { continuation -> return suspendCoroutine { continuation ->
messageContinuations[messageId] = encodingInfo to continuation messageContinuations[messageId] = MessageContinuation(encodingInfo, continuation, stacktrace)
} }
} }
@@ -0,0 +1,20 @@
/*
* Copyright 2010-2022 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.
*/
const vm = require('vm');
const fs = require('fs');
var testFilePath = process.argv[2];
const sandbox = {};
vm.createContext(sandbox);
const code = fs.readFileSync(testFilePath, 'utf-8');
try {
vm.runInContext(code, sandbox, testFilePath);
vm.runInContext('main.box()', sandbox);
} catch (e) {
// Ignore any exceptions
}
@@ -4,6 +4,7 @@
*/ */
package org.jetbrains.kotlin.js.test.handlers package org.jetbrains.kotlin.js.test.handlers
import kotlinx.coroutines.withTimeout
import org.jetbrains.kotlin.KtPsiSourceFileLinesMapping import org.jetbrains.kotlin.KtPsiSourceFileLinesMapping
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.TranslationMode import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.TranslationMode
import org.jetbrains.kotlin.js.parser.sourcemaps.* import org.jetbrains.kotlin.js.parser.sourcemaps.*
@@ -104,6 +105,8 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t
debugger.stepInto() debugger.stepInto()
waitForResumeEvent() waitForResumeEvent()
} }
debugger.resume()
waitForResumeEvent()
} }
} }
checkSteppingTestResult( checkSteppingTestResult(
@@ -216,7 +219,8 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t
*/ */
private class NodeJsDebuggerFacade(jsFilePath: String) { private class NodeJsDebuggerFacade(jsFilePath: String) {
private val inspector = NodeJsInspectorClient("js/js.translator/testData/runIrTestInNode.js", listOf(jsFilePath)) private val inspector =
NodeJsInspectorClient("js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js", listOf(jsFilePath))
private val scriptUrls = mutableMapOf<Runtime.ScriptId, String>() private val scriptUrls = mutableMapOf<Runtime.ScriptId, String>()
@@ -228,12 +232,15 @@ private class NodeJsDebuggerFacade(jsFilePath: String) {
is Debugger.Event.ScriptParsed -> { is Debugger.Event.ScriptParsed -> {
scriptUrls[event.scriptId] = event.url scriptUrls[event.scriptId] = event.url
} }
is Debugger.Event.Paused -> { is Debugger.Event.Paused -> {
pausedEvent = event pausedEvent = event
} }
is Debugger.Event.Resumed -> { is Debugger.Event.Resumed -> {
pausedEvent = null pausedEvent = null
} }
else -> {} else -> {}
} }
} }
@@ -248,7 +255,9 @@ private class NodeJsDebuggerFacade(jsFilePath: String) {
runtime.runIfWaitingForDebugger() runtime.runIfWaitingForDebugger()
waitForPauseEvent { it.reason == Debugger.PauseReason.BREAK_ON_START } waitForPauseEvent { it.reason == Debugger.PauseReason.BREAK_ON_START }
body() withTimeout(30000) {
body()
}
} }
fun scriptUrlByScriptId(scriptId: Runtime.ScriptId) = scriptUrls[scriptId] ?: error("unknown scriptId") fun scriptUrlByScriptId(scriptId: Runtime.ScriptId) = scriptUrls[scriptId] ?: error("unknown scriptId")