From 6b73ef4b0b3907eaabc36e45ca306f1c82855fdf Mon Sep 17 00:00:00 2001 From: Jiaxiang Chen Date: Thu, 17 Oct 2019 14:34:42 -0700 Subject: [PATCH] refactor stepping test to extract common logic for other debug information test --- .../debugInformation/AbstractDebugTest.kt | 233 +++++++++++++++++ .../debugInformation/AbstractSteppingTest.kt | 240 ++---------------- 2 files changed, 249 insertions(+), 224 deletions(-) create mode 100644 compiler/tests-common/tests/org/jetbrains/kotlin/codegen/debugInformation/AbstractDebugTest.kt diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/debugInformation/AbstractDebugTest.kt b/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/debugInformation/AbstractDebugTest.kt new file mode 100644 index 00000000000..5032359f7a3 --- /dev/null +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/debugInformation/AbstractDebugTest.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2010-2019 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. + */ + +package org.jetbrains.kotlin.codegen.debugInformation + +import com.intellij.openapi.util.SystemInfo +import com.intellij.util.PathUtil +import com.intellij.util.SystemProperties +import com.sun.jdi.VirtualMachine +import com.sun.jdi.event.* +import com.sun.jdi.request.EventRequest +import com.sun.jdi.request.EventRequest.SUSPEND_NONE +import com.sun.jdi.request.StepRequest +import com.sun.tools.jdi.SocketAttachingConnector +import org.jetbrains.kotlin.backend.common.output.SimpleOutputFileCollection +import org.jetbrains.kotlin.cli.common.output.writeAllTo +import org.jetbrains.kotlin.codegen.* +import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime +import org.jetbrains.kotlin.test.ConfigurationKind +import org.jetbrains.kotlin.test.KotlinTestUtils +import org.jetbrains.kotlin.test.clientserver.TestProcessServer +import org.jetbrains.kotlin.test.clientserver.TestProxy +import org.jetbrains.kotlin.test.clientserver.getGeneratedClass +import org.junit.After +import org.junit.Before +import java.io.File +import java.lang.IllegalStateException +import java.net.URLClassLoader +import kotlin.properties.Delegates + +data class ProcessAndPort(val process: Process, val port: Int) + +abstract class AbstractDebugTest : CodegenTestCase() { + + abstract val virtualMachine: VirtualMachine + abstract val proxyPort: Int + + companion object { + const val DEBUG_ADDRESS = "127.0.0.1" + const val MAIN_CLASS = "org.jetbrains.kotlin.test.clientserver.TestProcessServer" + const val TEST_CLASS = "TestKt" + const val BOX_METHOD = "box" + + fun setUpVM(virtualMachine: VirtualMachine) { + val manager = virtualMachine.eventRequestManager() + + val methodEntryReq = manager.createMethodEntryRequest() + methodEntryReq.addClassFilter(TEST_CLASS) + methodEntryReq.setSuspendPolicy(EventRequest.SUSPEND_ALL) + methodEntryReq.enable() + + val methodExitReq = manager.createMethodExitRequest() + methodExitReq.addClassFilter(TEST_CLASS) + methodExitReq.setSuspendPolicy(EventRequest.SUSPEND_NONE) + methodExitReq.enable() + } + + fun getProxyPort(process: Process): Int { + val reader = process.inputStream.bufferedReader() + reader.readLine() + val proxyPort = reader.readLine() + .split("port ") + .last() + .trim() + .toInt() + reader.close() + return proxyPort + } + + fun startDebuggeeProcess(): ProcessAndPort { + val classpath = listOf( + PathUtil.getJarPathForClass(TestProcessServer::class.java), + PathUtil.getJarPathForClass(Delegates::class.java) // Add Kotlin runtime JAR + ) + + val javaExecutablePath = findJavaExecutable().absolutePath + val command = arrayOf( + if (SystemInfo.isWindows) "\"$javaExecutablePath\"" else javaExecutablePath, + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:0", + "-ea", + "-classpath", classpath.joinToString(File.pathSeparator), + MAIN_CLASS, + TestProcessServer.DEBUG_TEST + ) + + val process = ProcessBuilder(*command).start() + val port = process.inputStream.bufferedReader().readLine() + .split("address:") + .last() + .trim() + .toInt() + return ProcessAndPort(process, port) + } + + fun attachDebugger(port: Int): VirtualMachine { + val connector = SocketAttachingConnector() + return connector.attach(connector.defaultArguments().toMutableMap().apply { + getValue("port").setValue("$port") + getValue("hostname").setValue(DEBUG_ADDRESS) + }) + } + + fun findJavaExecutable(): File { + val javaBin = File(SystemProperties.getJavaHome(), "bin") + return File(javaBin, "java.exe").takeIf { it.exists() } + ?: File(javaBin, "java").also { assert(it.exists()) } + } + + } + + @Before + public override fun setUp() { + super.setUp() + } + + @After + public override fun tearDown() { + super.tearDown() + } + + internal fun invokeBoxInSeparateProcess(classLoader: URLClassLoader, aClass: Class<*>, port: Int): String { + + val classPath = classLoader.extractUrls().toMutableList() + if (classLoader is GeneratedClassLoader) { + val outDir = KotlinTestUtils.tmpDirForTest(this) + val currentOutput = SimpleOutputFileCollection(classLoader.allGeneratedFiles) + currentOutput.writeAllTo(outDir) + classPath.add(0, outDir.toURI().toURL()) + } + + return TestProxy(Integer.valueOf(port), aClass.canonicalName, classPath).runTestNoOutput() + } + + internal fun createGeneratedClassLoader(classesDir: File): URLClassLoader { + return URLClassLoader( + listOf(classesDir.toURI().toURL()).toTypedArray(), + ForTestCompileRuntime.runtimeJarClassLoader() + ) + } + + override fun doMultiFileTest(wholeFile: File, files: List) { + createEnvironmentWithMockJdkAndIdeaAnnotations( + ConfigurationKind.ALL, *listOfNotNull( + writeJavaFiles( + files + ) + ).toTypedArray() + ) + + loadMultiFiles(files) + val classBuilderFactory = + OriginCollectingClassBuilderFactory(ClassBuilderMode.FULL) + val generationState = + GenerationUtils.compileFiles(myFiles.psiFiles, myEnvironment, classBuilderFactory) + classFileFactory = generationState.factory + + val tempDirForTest = KotlinTestUtils.tmpDir("debuggerTest") + val classesDir = File(tempDirForTest, "classes") + try { + classFileFactory.writeAllTo(classesDir) + doTest(wholeFile, classesDir) + } finally { + tempDirForTest.deleteRecursively() + } + } + + protected open fun doTest(wholeFile: File, classesDir: File) { + + val classLoader = createGeneratedClassLoader(classesDir) + val aClass = getGeneratedClass(classLoader, TEST_CLASS) + invokeBoxInSeparateProcess(classLoader, aClass, proxyPort) + + val manager = virtualMachine.eventRequestManager() + + val loggedItems = ArrayList() + var inBoxMethod = false + vmLoop@ + while (true) { + val eventSet = virtualMachine.eventQueue().remove() + for (event in eventSet) { + when (event) { + is VMDeathEvent, is VMDisconnectEvent -> { + break@vmLoop + } + // We start VM with option 'suspend=n', in case VMStartEvent is still received, discard. + is VMStartEvent -> { + + } + is MethodEntryEvent -> { + if (!inBoxMethod && event.location().method().name() == BOX_METHOD) { + if (manager.stepRequests().isEmpty()) { + val stepReq = manager.createStepRequest(event.thread(), StepRequest.STEP_LINE, StepRequest.STEP_INTO) + stepReq.setSuspendPolicy(SUSPEND_NONE) + stepReq.addClassExclusionFilter("java.*") + stepReq.addClassExclusionFilter("sun.*") + stepReq.addClassExclusionFilter("kotlin.*") + } + manager.stepRequests().map { it.enable() } + inBoxMethod = true + storeStep(loggedItems, event) + } + virtualMachine.resume() + } + is StepEvent -> { + // Handle the case where an Exception causing program to exit without MethodExitEvent. + if (inBoxMethod && event.location().method().name() == "run") { + break@vmLoop + } + if (inBoxMethod) { + storeStep(loggedItems, event) + } + } + is MethodExitEvent -> { + if (event.location().method().name() == BOX_METHOD) { + manager.stepRequests().map { it.disable() } + break@vmLoop + } + } + else -> { + throw IllegalStateException("event not handled: $event") + } + } + } + } + checkResult(wholeFile, loggedItems) + } + + abstract fun storeStep(loggedItems: ArrayList, event: Event) + + abstract fun checkResult(wholeFile: File, loggedItems: List) +} \ No newline at end of file diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/debugInformation/AbstractSteppingTest.kt b/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/debugInformation/AbstractSteppingTest.kt index 06d30b3e6c9..cdb07d6d382 100644 --- a/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/debugInformation/AbstractSteppingTest.kt +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/codegen/debugInformation/AbstractSteppingTest.kt @@ -5,43 +5,20 @@ package org.jetbrains.kotlin.codegen.debugInformation -import com.intellij.openapi.util.SystemInfo -import com.intellij.util.PathUtil -import com.intellij.util.SystemProperties import com.sun.jdi.VirtualMachine -import com.sun.jdi.event.* -import com.sun.jdi.request.EventRequest.SUSPEND_ALL -import com.sun.jdi.request.EventRequest.SUSPEND_NONE -import com.sun.jdi.request.StepRequest -import com.sun.tools.jdi.SocketAttachingConnector +import com.sun.jdi.event.Event +import com.sun.jdi.event.LocatableEvent import junit.framework.TestCase -import org.jetbrains.kotlin.backend.common.output.OutputFile -import org.jetbrains.kotlin.backend.common.output.SimpleOutputFileCollection -import org.jetbrains.kotlin.cli.common.output.writeAllTo -import org.jetbrains.kotlin.codegen.* -import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime -import org.jetbrains.kotlin.codegen.state.GenerationState -import org.jetbrains.kotlin.test.ConfigurationKind -import org.jetbrains.kotlin.test.KotlinTestUtils -import org.jetbrains.kotlin.test.clientserver.getGeneratedClass -import org.jetbrains.kotlin.test.clientserver.TestProcessServer -import org.jetbrains.kotlin.test.clientserver.TestProxy -import org.junit.After import org.junit.AfterClass -import org.junit.Before import org.junit.BeforeClass import java.io.File -import java.lang.IllegalStateException -import java.net.URLClassLoader -import kotlin.properties.Delegates -abstract class AbstractSteppingTest : CodegenTestCase() { +abstract class AbstractSteppingTest : AbstractDebugTest() { + + override val virtualMachine: VirtualMachine = Companion.virtualMachine + override val proxyPort: Int = Companion.proxyPort companion object { - const val DEBUG_ADDRESS = "127.0.0.1" - const val MAIN_CLASS = "org.jetbrains.kotlin.test.clientserver.TestProcessServer" - const val TEST_CLASS = "TestKt" - const val BOX_METHOD = "box" const val LINENUMBER_PREFIX = "// LINENUMBERS" var proxyPort = 0 lateinit var process: Process @@ -50,70 +27,12 @@ abstract class AbstractSteppingTest : CodegenTestCase() { @BeforeClass @JvmStatic fun setUpTest() { - val debugPort = startDebuggeeProcess() - virtualMachine = attachDebugger(debugPort) + val (process, port) = startDebuggeeProcess() + this.process = process + virtualMachine = attachDebugger(port) setUpVM(virtualMachine) - val reader = process.inputStream.bufferedReader() - reader.readLine() - proxyPort = reader.readLine() - .split("port ") - .last() - .trim() - .toInt() - reader.close() - } - - private fun setUpVM(virtualMachine: VirtualMachine) { - val manager = virtualMachine.eventRequestManager() - - val methodEntryReq = manager.createMethodEntryRequest() - methodEntryReq.addClassFilter(TEST_CLASS) - methodEntryReq.setSuspendPolicy(SUSPEND_ALL) - methodEntryReq.enable() - - val methodExitReq = manager.createMethodExitRequest() - methodExitReq.addClassFilter(TEST_CLASS) - methodExitReq.setSuspendPolicy(SUSPEND_NONE) - methodExitReq.enable() - } - - private fun startDebuggeeProcess(): Int { - val classpath = listOf( - PathUtil.getJarPathForClass(TestProcessServer::class.java), - PathUtil.getJarPathForClass(Delegates::class.java) // Add Kotlin runtime JAR - ) - - val javaExecutablePath = findJavaExecutable().absolutePath - val command = arrayOf( - if (SystemInfo.isWindows) "\"$javaExecutablePath\"" else javaExecutablePath, - "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:0", - "-ea", - "-classpath", classpath.joinToString(File.pathSeparator), - MAIN_CLASS, - TestProcessServer.DEBUG_TEST - ) - - process = ProcessBuilder(*command).start() - return process.inputStream.bufferedReader().readLine() - .split("address:") - .last() - .trim() - .toInt() - } - - private fun attachDebugger(port: Int): VirtualMachine { - val connector = SocketAttachingConnector() - return connector.attach(connector.defaultArguments().toMutableMap().apply { - getValue("port").setValue("$port") - getValue("hostname").setValue(DEBUG_ADDRESS) - }) - } - - private fun findJavaExecutable(): File { - val javaBin = File(SystemProperties.getJavaHome(), "bin") - return File(javaBin, "java.exe").takeIf { it.exists() } - ?: File(javaBin, "java").also { assert(it.exists()) } + proxyPort = getProxyPort(process) } @AfterClass @@ -123,150 +42,23 @@ abstract class AbstractSteppingTest : CodegenTestCase() { } } - @Before - public override fun setUp() { - super.setUp() + override fun storeStep(loggedItems: ArrayList, event: Event) { + assert(event is LocatableEvent) + loggedItems.add(event) } - @After - public override fun tearDown() { - super.tearDown() - } - - override fun doMultiFileTest(wholeFile: File, files: List) { - createEnvironmentWithMockJdkAndIdeaAnnotations( - ConfigurationKind.ALL, *listOfNotNull( - writeJavaFiles( - files - ) - ).toTypedArray() - ) - - val lineNumbers = wholeFile + override fun checkResult(wholeFile: File, loggedItems: List) { + val expectedLineNumbers = wholeFile .readLines() .dropWhile { !it.startsWith(LINENUMBER_PREFIX) } .drop(1) .map { it.drop(3).trim() } .joinToString(" ") - - loadMultiFiles(files) - val classBuilderFactory = - OriginCollectingClassBuilderFactory(ClassBuilderMode.FULL) - val generationState = - GenerationUtils.compileFiles(myFiles.psiFiles, myEnvironment, classBuilderFactory) - classFileFactory = generationState.factory - - val tempDirForTest = KotlinTestUtils.tmpDir("debuggerTest") - val classesDir = File(tempDirForTest, "classes") - try { - classFileFactory.writeAllTo(classesDir) - - try { - doTest(lineNumbers, classesDir, virtualMachine, classBuilderFactory, classFileFactory, generationState) - } finally { - - } - } finally { - tempDirForTest.deleteRecursively() - } - } - - private fun invokeBoxInSeparateProcess(classLoader: URLClassLoader, aClass: Class<*>, port: Int): String { - - val classPath = classLoader.extractUrls().toMutableList() - if (classLoader is GeneratedClassLoader) { - val outDir = KotlinTestUtils.tmpDirForTest(this) - val currentOutput = SimpleOutputFileCollection(classLoader.allGeneratedFiles) - currentOutput.writeAllTo(outDir) - classPath.add(0, outDir.toURI().toURL()) - } - - return TestProxy(Integer.valueOf(port), aClass.canonicalName, classPath).runTestNoOutput() - } - - private fun createGeneratedClassLoader(classesDir: File): URLClassLoader { - return URLClassLoader( - listOf(classesDir.toURI().toURL()).toTypedArray(), - ForTestCompileRuntime.runtimeJarClassLoader() - ) - } - - protected open fun doTest( - expectedLineNumbers: String, - classesDir: File, - virtualMachine: VirtualMachine, - factory: OriginCollectingClassBuilderFactory, - classFileFactory: ClassFileFactory, - state: GenerationState - ) { - - val classLoader = createGeneratedClassLoader(classesDir) - val aClass = getGeneratedClass(classLoader, TEST_CLASS) - - val manager = virtualMachine.eventRequestManager() - - invokeBoxInSeparateProcess(classLoader, aClass, proxyPort) - val loggedEvents = ArrayList() - var shouldRecordSteps = false - vmLoop@ - while (true) { - val eventSet = virtualMachine.eventQueue().remove() - for (event in eventSet) { - when (event) { - is VMDeathEvent, is VMDisconnectEvent -> { - break@vmLoop - } - // We start VM with option 'suspend=n', in case VMStartEvent is still received, discard. - is VMStartEvent -> { - - } - is MethodEntryEvent -> { - if (!shouldRecordSteps && event.location().method().name() == BOX_METHOD) { - if (manager.stepRequests().isEmpty()) { - val stepReq = manager.createStepRequest(event.thread(), StepRequest.STEP_LINE, StepRequest.STEP_INTO) - stepReq.setSuspendPolicy(SUSPEND_NONE) - stepReq.addClassExclusionFilter("java.*") - stepReq.addClassExclusionFilter("sun.*") - stepReq.addClassExclusionFilter("kotlin.*") - } - manager.stepRequests().map { it.enable() } - shouldRecordSteps = true - loggedEvents.add(event) - } - virtualMachine.resume() - } - is StepEvent -> { - if (shouldRecordSteps && event.location().method().name() == "run") { - break@vmLoop - } - if (shouldRecordSteps) { - loggedEvents.add(event) - } - } - is MethodExitEvent -> { - if (event.location().method().name() == BOX_METHOD) { - manager.stepRequests().map { it.disable() } - break@vmLoop - } - } - else -> { - throw IllegalStateException("event not handled: $event") - } - } - } - } - - val actualLineNumbers = loggedEvents + val actualLineNumbers = loggedItems .map { event -> "${(event as LocatableEvent).location().method()}:${event.location().lineNumber()}" } TestCase.assertEquals(expectedLineNumbers, actualLineNumbers.joinToString(" ")) } - - internal val OutputFile.internalName - get() = relativePath.substringBeforeLast(".class") - - internal val OutputFile.qualifiedName - get() = internalName.replace('/', '.') }