[JS IR] Use a special executor for stepping tests
This will hopefully fix flakiness for some tests
This commit is contained in:
+26
-1
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user