diff --git a/compiler/testData/debug/stepping/conjunction.kt b/compiler/testData/debug/stepping/conjunction.kt index 232830342cd..2d8a9e724fa 100644 --- a/compiler/testData/debug/stepping/conjunction.kt +++ b/compiler/testData/debug/stepping/conjunction.kt @@ -34,4 +34,11 @@ fun getD() = true // EXPECTATIONS JS_IR // test.kt:3 box -// test.kt:11 box \ No newline at end of file +// test.kt:13 getA +// test.kt:4 box +// test.kt:15 getB +// test.kt:5 box +// test.kt:17 getC +// test.kt:9 box +// test.kt:3 box +// test.kt:11 box diff --git a/compiler/testData/debug/stepping/for.kt b/compiler/testData/debug/stepping/for.kt index f5ff44c680a..4fdebb22ea3 100644 --- a/compiler/testData/debug/stepping/for.kt +++ b/compiler/testData/debug/stepping/for.kt @@ -31,4 +31,5 @@ inline fun foo(n: Int) {} // test.kt:3 box // test.kt:3 box // test.kt:3 box -// test.kt:6 box \ No newline at end of file +// test.kt:3 box +// test.kt:6 box diff --git a/compiler/testData/debug/stepping/ifWithInlineInCondition.kt b/compiler/testData/debug/stepping/ifWithInlineInCondition.kt index 2da0c4d4bf3..dcb85cdadd3 100644 --- a/compiler/testData/debug/stepping/ifWithInlineInCondition.kt +++ b/compiler/testData/debug/stepping/ifWithInlineInCondition.kt @@ -56,6 +56,7 @@ fun nop() {} // test.kt:21 box // EXPECTATIONS JS_IR +// test.kt:24 box // test.kt:3 box // test.kt:4 box // test.kt:30 nop @@ -74,4 +75,4 @@ fun nop() {} // test.kt:15 box // test.kt:19 box // test.kt:30 nop -// test.kt:21 box \ No newline at end of file +// test.kt:21 box diff --git a/compiler/testData/debug/stepping/jsCode.kt b/compiler/testData/debug/stepping/jsCode.kt index 1a032e118c4..d1bc475a514 100644 --- a/compiler/testData/debug/stepping/jsCode.kt +++ b/compiler/testData/debug/stepping/jsCode.kt @@ -33,10 +33,10 @@ val jesse1VarDecl = "var jesse1 = _.exclamate(jesse);" // FILE: test.kt -fun noop() {} + fun box() { - noop() + exclamateAndConcat( "hello", "world") @@ -56,8 +56,6 @@ fun box() { } // EXPECTATIONS -// test.kt:39 box -// test.kt:36 noop // test.kt:41 box // test.kt:42 box // a.kt:11 box @@ -79,4 +77,4 @@ fun box() { // a.kt:6 exclamate // a.kt:29 box // a.kt:22 value -// test.kt:56 box \ No newline at end of file +// test.kt:56 box diff --git a/compiler/testData/debug/stepping/kt42208.kt b/compiler/testData/debug/stepping/kt42208.kt index eb3e64c0ce2..f32f6f2d7fc 100644 --- a/compiler/testData/debug/stepping/kt42208.kt +++ b/compiler/testData/debug/stepping/kt42208.kt @@ -19,6 +19,7 @@ inline fun foo() = { // test.kt:5 box // EXPECTATIONS JS_IR +// test1.kt:9 box // test.kt:4 box // test1.kt:6 box$lambda -// test.kt:5 box \ No newline at end of file +// test.kt:5 box diff --git a/compiler/testData/debug/stepping/kt42208b.kt b/compiler/testData/debug/stepping/kt42208b.kt index f17b728d31c..fca580b5c5d 100644 --- a/compiler/testData/debug/stepping/kt42208b.kt +++ b/compiler/testData/debug/stepping/kt42208b.kt @@ -25,7 +25,8 @@ inline fun foo() = { // test.kt:6 box // EXPECTATIONS JS_IR +// test1.kt:10 box // test.kt:4 box // test.kt:5 box // test1.kt:7 box$lambda -// test.kt:6 box \ No newline at end of file +// test.kt:6 box diff --git a/compiler/testData/debug/stepping/kt42208c.kt b/compiler/testData/debug/stepping/kt42208c.kt index 40fe0bf3138..2f41d9d3171 100644 --- a/compiler/testData/debug/stepping/kt42208c.kt +++ b/compiler/testData/debug/stepping/kt42208c.kt @@ -38,6 +38,7 @@ fun baz(v:(() -> Unit)) { // test.kt:7 box // EXPECTATIONS JS_IR +// test1.kt:11 box // test.kt:4 box // test3.kt:14 baz // test1.kt:8 box$lambda @@ -48,4 +49,4 @@ fun baz(v:(() -> Unit)) { // test3.kt:14 baz // test1.kt:8 box$lambda // test3.kt:15 baz -// test.kt:7 box \ No newline at end of file +// test.kt:7 box diff --git a/compiler/testData/debug/stepping/nestedInline.kt b/compiler/testData/debug/stepping/nestedInline.kt index 428dfdd6f45..213c603c8ff 100644 --- a/compiler/testData/debug/stepping/nestedInline.kt +++ b/compiler/testData/debug/stepping/nestedInline.kt @@ -65,5 +65,12 @@ inline fun html(init: () -> Unit) { // test.kt:21 box // EXPECTATIONS JS_IR +// test.kt:7 box +// 1.kt:33 box +// 1.kt:36 box +// 1.kt:37 box +// 1.kt:37 box +// 1.kt:41 box +// test.kt:15 box // test.kt:19 box // test.kt:21 box diff --git a/compiler/testData/debug/stepping/simpleInlineDefaultArg.kt b/compiler/testData/debug/stepping/simpleInlineDefaultArg.kt index 89cd1d1415a..4a97d1469ea 100644 --- a/compiler/testData/debug/stepping/simpleInlineDefaultArg.kt +++ b/compiler/testData/debug/stepping/simpleInlineDefaultArg.kt @@ -38,3 +38,6 @@ fun box(): String { // test.kt:10 box // EXPECTATIONS JS_IR +// test.kt:3 box +// test.kt:5 box +// test.kt:6 box diff --git a/compiler/testData/debug/stepping/smapInlineAsArgument.kt b/compiler/testData/debug/stepping/smapInlineAsArgument.kt index 9cf88392bf8..ed940aac5bd 100644 --- a/compiler/testData/debug/stepping/smapInlineAsArgument.kt +++ b/compiler/testData/debug/stepping/smapInlineAsArgument.kt @@ -38,6 +38,7 @@ fun fail() : String { // test.kt:9 box // EXPECTATIONS JS_IR +// test.kt:16 box // test.kt:4 box // test.kt:20 fail // test.kt:4 box @@ -47,4 +48,4 @@ fun fail() : String { // test.kt:16 box // test.kt:7 box // test.kt:13 checkEquals -// test.kt:9 box \ No newline at end of file +// test.kt:9 box diff --git a/compiler/testData/debug/stepping/smapInlineAsInfixArgument.kt b/compiler/testData/debug/stepping/smapInlineAsInfixArgument.kt index cf27fa42587..3016f202b61 100644 --- a/compiler/testData/debug/stepping/smapInlineAsInfixArgument.kt +++ b/compiler/testData/debug/stepping/smapInlineAsInfixArgument.kt @@ -36,6 +36,7 @@ fun fail() : String { // test.kt:11 box // EXPECTATIONS JS_IR +// test.kt:14 box // test.kt:6 box // test.kt:18 fail // test.kt:6 box @@ -45,4 +46,4 @@ fun fail() : String { // test.kt:14 box // test.kt:9 box // test.kt:3 execute -// test.kt:11 box \ No newline at end of file +// test.kt:11 box diff --git a/compiler/testData/debug/stepping/smapInlineAsInlineArgument.kt b/compiler/testData/debug/stepping/smapInlineAsInlineArgument.kt index ce5005fd043..7db294a592b 100644 --- a/compiler/testData/debug/stepping/smapInlineAsInlineArgument.kt +++ b/compiler/testData/debug/stepping/smapInlineAsInlineArgument.kt @@ -38,6 +38,7 @@ fun fail() : String { // test.kt:9 box // EXPECTATIONS JS_IR +// test.kt:16 box // test.kt:4 box // test.kt:5 box // test.kt:20 fail @@ -47,4 +48,4 @@ fun fail() : String { // test.kt:16 box // test.kt:8 box // test.kt:16 box -// test.kt:9 box \ No newline at end of file +// test.kt:9 box diff --git a/compiler/testData/debug/stepping/smapInlineInIntrinsicArgument.kt b/compiler/testData/debug/stepping/smapInlineInIntrinsicArgument.kt index a5bdaaeb6e3..3f39d8c34a7 100644 --- a/compiler/testData/debug/stepping/smapInlineInIntrinsicArgument.kt +++ b/compiler/testData/debug/stepping/smapInlineInIntrinsicArgument.kt @@ -37,10 +37,11 @@ fun fail() : String { // test.kt:9 box // EXPECTATIONS JS_IR +// test.kt:12 box // test.kt:4 box // test.kt:16 fail // test.kt:7 box // test.kt:16 fail // test.kt:12 box // test.kt:7 box -// test.kt:9 box \ No newline at end of file +// test.kt:9 box diff --git a/compiler/testData/debug/stepping/suspendFunWithLambdaParameter.kt b/compiler/testData/debug/stepping/suspendFunWithLambdaParameter.kt index 851d401dfed..461d6adeff7 100644 --- a/compiler/testData/debug/stepping/suspendFunWithLambdaParameter.kt +++ b/compiler/testData/debug/stepping/suspendFunWithLambdaParameter.kt @@ -19,7 +19,13 @@ suspend fun box() { // test.kt:11 box // EXPECTATIONS JS_IR +// test.kt:7 box +// test.kt:7 +// test.kt:7 +// test.kt:7 box +// test.kt:7 box +// test.kt:7 box // test.kt:8 doResume // test.kt:4 foo // test.kt:4 foo -// test.kt:9 box$lambda \ No newline at end of file +// test.kt:9 box$lambda diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js b/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js index 776f2e694f3..aca1469d365 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/debugger/stepping_test_executor.js @@ -14,7 +14,13 @@ const code = fs.readFileSync(testFilePath, 'utf-8'); try { vm.runInContext(code, sandbox, testFilePath); - vm.runInContext('main.box()', sandbox); + // language=JavaScript + vm.runInContext(` + debugger; + main.box(); + `, + sandbox + ); } catch (e) { // Ignore any exceptions } 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 929914ba39f..1c30d2875a9 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 @@ -5,16 +5,12 @@ package org.jetbrains.kotlin.js.test.handlers import kotlinx.coroutines.withTimeout -import org.jetbrains.kotlin.KtPsiSourceFileLinesMapping import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.TranslationMode import org.jetbrains.kotlin.js.parser.sourcemaps.* import org.jetbrains.kotlin.js.test.debugger.* import org.jetbrains.kotlin.js.test.utils.getAllFilesForRunner -import org.jetbrains.kotlin.js.test.utils.getBoxFunction -import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.test.TargetBackend import org.jetbrains.kotlin.test.directives.JsEnvironmentConfigurationDirectives -import org.jetbrains.kotlin.test.model.TestFile import org.jetbrains.kotlin.test.model.TestModule import org.jetbrains.kotlin.test.services.TestServices import org.jetbrains.kotlin.test.services.configuration.JsEnvironmentConfigurator @@ -29,7 +25,7 @@ import java.net.URISyntaxException /** * This class is an analogue of the [DebugRunner][org.jetbrains.kotlin.test.backend.handlers.DebugRunner] from JVM stepping tests. * - * It runs a generated JavaScript file under a debugger, sets a breakpoint in the beginning of the `box` function + * It runs a generated JavaScript file under a debugger, stops right before entering the `box` function, * and performs the "step into" action until there is nothing more to step into. On each pause it records the source file name, * the source line and the function name of the current call frame, and compares this data with the expectations written in the test file. * @@ -72,47 +68,54 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t sourceMap: SourceMap, mainModule: TestModule, ) { - val (testFileWithBoxFunction, boxFunctionStartLine) = getBoxFunctionStartLocation(mainModule) - val originalFileWithBoxFunction = testFileWithBoxFunction.originalFile - - val boxFunctionLineInGeneratedFile = - sourceMap.breakpointLineInGeneratedFile(originalFileWithBoxFunction, boxFunctionStartLine) - - if (boxFunctionLineInGeneratedFile < 0) - error("Could not find the location of the 'box' function in the generated file") - + val originalFile = mainModule.files.first { !it.isAdditional }.originalFile val debuggerFacade = NodeJsDebuggerFacade(jsFilePath) - val jsFileURI = File(jsFilePath).absoluteFile.toURI().withAuthority("") + val jsFile = File(jsFilePath) + + val jsFileURI = jsFile.makeURI() val loggedItems = mutableListOf() + debuggerFacade.run { - with(debuggerFacade) { - val boxFunctionBreakpoint = debugger.setBreakpointByUrl(boxFunctionLineInGeneratedFile, jsFileURI.toString()) - debugger.resume() - waitForResumeEvent() - waitForPauseEvent { - it.reason == Debugger.PauseReason.OTHER && it.hitBreakpoints.contains(boxFunctionBreakpoint.breakpointId) - } + debugger.resume() + waitForResumeEvent() + waitForPauseEvent { + it.reason == Debugger.PauseReason.OTHER // hit the 'debugger' statement + } + + suspend fun repeatedlyStepInto(action: suspend (Debugger.CallFrame) -> Boolean) { while (true) { val topMostCallFrame = waitForPauseEvent().callFrames[0] - try { - if (URI(scriptUrlByScriptId(topMostCallFrame.location.scriptId)) != jsFileURI) break - } catch (_: URISyntaxException) { - // Probably something like 'evalmachine.' brought us here. Ignore. - } - addCallFrameInfoToLoggedItems(sourceMap, topMostCallFrame, loggedItems) + if (!action(topMostCallFrame)) break debugger.stepInto() waitForResumeEvent() } - debugger.resume() - waitForResumeEvent() } + + fun Debugger.CallFrame.isInFileUnderTest() = try { + URI(scriptUrlByScriptId(location.scriptId)) == jsFileURI + } catch (_: URISyntaxException) { + false + } + + repeatedlyStepInto { + !it.isInFileUnderTest() + } + repeatedlyStepInto { callFrame -> + callFrame.isInFileUnderTest().also { + if (it) + addCallFrameInfoToLoggedItems(sourceMap, callFrame, loggedItems) + } + } + + debugger.resume() + waitForResumeEvent() } checkSteppingTestResult( mainModule.frontendKind, mainModule.targetBackend ?: TargetBackend.JS_IR, - originalFileWithBoxFunction, + originalFile, loggedItems ) } @@ -123,82 +126,24 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t loggedItems: MutableList ) { val originalFunctionName = topMostCallFrame.functionLocation?.let { - sourceMap.getSourceLineForGeneratedLocation(it)?.name + sourceMap.segmentForGeneratedLocation(it.lineNumber, it.columnNumber)?.name } - sourceMap.getSourceLineForGeneratedLocation(topMostCallFrame.location)?.let { (_, sourceFile, sourceLine, _, _) -> + sourceMap.segmentForGeneratedLocation( + topMostCallFrame.location.lineNumber, + topMostCallFrame.location.columnNumber + )?.let { (_, sourceFile, sourceLine, _, _) -> if (sourceFile == null || sourceLine < 0) return@let val testFileName = testFileNameFromMappedLocation(sourceFile, sourceLine) ?: return - val expectation = - formatAsSteppingTestExpectation(testFileName, sourceLine + 1, originalFunctionName ?: topMostCallFrame.functionName, false) + val expectation = formatAsSteppingTestExpectation( + testFileName, + sourceLine + 1, + originalFunctionName ?: topMostCallFrame.functionName, + false, + ) loggedItems.add(SteppingTestLoggedData(sourceLine + 1, false, expectation)) } } - /** - * Returns the test file and the line number in that file where the body of the `box` function begins. - */ - private fun getBoxFunctionStartLocation(mainModule: TestModule): Pair { - val boxFunction = getBoxFunction(testServices) ?: error("Missing 'box' function") - val file = boxFunction.containingKtFile - val mapping = KtPsiSourceFileLinesMapping(file) - val firstStatementOffset = boxFunction.bodyBlockExpression?.firstStatement?.startOffset - ?: boxFunction.bodyExpression?.startOffset - ?: boxFunction.startOffset - return mainModule.files.single { it.name == file.name } to mapping.getLineByOffset(firstStatementOffset) - } - - /** - * Maps the location in the source file to the location in the generated file. - * - * The Node.js debugger is not sourcemap-aware, so we need to set a breakpoint in the `box` function in the generated JS file. - * - * We don't know where the generated `box` function is located, so we use the source map to figure it out. - * - * This is basically what Intellij IDEA's built-in JavaScript debugger does when you set a breakpoint in a source file: it tries - * to map the location of the breakpoint in the source file to a location in the generated file. Here we use a simplified - * algorithm for that. - */ - private fun SourceMap.breakpointLineInGeneratedFile(sourceFile: File, sourceLine: Int): Int { - val sourceFileAbsolutePath = sourceFile.absoluteFile.normalize() - var candidateSegment: Pair? = null - for ((generatedLineNumber, group) in groups.withIndex()) { - for (segment in group.segments) { - if (segment.sourceFileName?.let { File(it).absoluteFile.normalize() } != sourceFileAbsolutePath || - segment.sourceLineNumber != sourceLine) - continue - if (candidateSegment == null) - candidateSegment = generatedLineNumber to segment - // Find the segment that points to the earliest column in the source file - if (segment.sourceColumnNumber < candidateSegment.second.sourceColumnNumber) - candidateSegment = generatedLineNumber to segment - } - } - return candidateSegment?.first ?: -1 - } - - /** - * Maps [location] in the generated JavaScript file to the corresponding location in a source file. - * @return The source file path (as specified in the source map) and the line number in that source file. - */ - private fun SourceMap.getSourceLineForGeneratedLocation(location: Debugger.Location): SourceMapSegment? { - - val group = groups.getOrNull(location.lineNumber)?.takeIf { it.segments.isNotEmpty() } ?: return null - val columnNumber = location.columnNumber ?: return group.segments[0] - return if (columnNumber <= group.segments[0].generatedColumnNumber) { - group.segments[0] - } else { - val candidateIndex = group.segments.indexOfFirst { - columnNumber <= it.generatedColumnNumber - } - if (candidateIndex < 0) - null - else if (candidateIndex == 0 || group.segments[candidateIndex].generatedColumnNumber == columnNumber) - group.segments[candidateIndex] - else - group.segments[candidateIndex - 1] - } - } - /** * An original test file may represent multiple source files (by using the `// FILE: myFile.kt` comments). * Sourcemaps contain paths to original test files. However, in test expectations we write names as in the `// FILE:` comments. @@ -206,9 +151,10 @@ class JsDebugRunner(testServices: TestServices) : AbstractJsArtifactsCollector(t */ private fun testFileNameFromMappedLocation(originalFilePath: String, originalFileLineNumber: Int): String? { val originalFile = File(originalFilePath) - return testServices.moduleStructure.modules.flatMap { it.files }.findLast { - it.originalFile.absolutePath == originalFile.absolutePath && it.startLineNumberInOriginalFile <= originalFileLineNumber - }?.name + return testServices.moduleStructure.modules.asSequence().flatMap { module -> module.files.asSequence().filter { !it.isAdditional } } + .findLast { + it.originalFile.absolutePath == originalFile.absolutePath && it.startLineNumberInOriginalFile <= originalFileLineNumber + }?.name } } @@ -250,26 +196,52 @@ private class NodeJsDebuggerFacade(jsFilePath: String) { /** * By the time [body] is called, the execution is paused, no code is executed yet. */ - fun run(body: suspend NodeJsInspectorClientContext.() -> T) = inspector.run { + fun run(body: suspend Context.() -> T) = inspector.run { debugger.enable() debugger.setSkipAllPauses(false) runtime.runIfWaitingForDebugger() - waitForPauseEvent { it.reason == Debugger.PauseReason.BREAK_ON_START } - withTimeout(30000) { - body() + with(Context(this)) { + waitForPauseEvent { it.reason == Debugger.PauseReason.BREAK_ON_START } + + withTimeout(30000) { + body() + } } } - fun scriptUrlByScriptId(scriptId: Runtime.ScriptId) = scriptUrls[scriptId] ?: error("unknown scriptId") + inner class Context(private val underlying: NodeJsInspectorClientContext) : NodeJsInspectorClientContext by underlying { - suspend fun NodeJsInspectorClientContext.waitForPauseEvent(suchThat: (Debugger.Event.Paused) -> Boolean = { true }) = - waitForValueToBecomeNonNull { - pausedEvent?.takeIf(suchThat) - } + fun scriptUrlByScriptId(scriptId: Runtime.ScriptId) = scriptUrls[scriptId] ?: error("unknown scriptId $scriptId") - suspend fun NodeJsInspectorClientContext.waitForResumeEvent() = waitForConditionToBecomeTrue { pausedEvent == null } + suspend fun waitForPauseEvent(suchThat: (Debugger.Event.Paused) -> Boolean = { true }) = + waitForValueToBecomeNonNull { + pausedEvent?.takeIf(suchThat) + } + + suspend fun waitForResumeEvent() = waitForConditionToBecomeTrue { pausedEvent == null } + } } +private fun File.makeURI(): URI = absoluteFile.toURI().withAuthority("") + private fun URI.withAuthority(newAuthority: String?) = URI(scheme, newAuthority, path, query, fragment) + +private fun SourceMap.segmentForGeneratedLocation(lineNumber: Int, columnNumber: Int?): SourceMapSegment? { + + val group = groups.getOrNull(lineNumber)?.takeIf { it.segments.isNotEmpty() } ?: return null + return if (columnNumber == null || columnNumber <= group.segments[0].generatedColumnNumber) { + group.segments[0] + } else { + val candidateIndex = group.segments.indexOfFirst { + columnNumber <= it.generatedColumnNumber + } + if (candidateIndex < 0) + null + else if (candidateIndex == 0 || group.segments[candidateIndex].generatedColumnNumber == columnNumber) + group.segments[candidateIndex] + else + group.segments[candidateIndex - 1] + } +}