Add a JVM backend debug information test, this commit is for verifying

line numbers for stepping.

Running JVM instance and read stepping events from it to verify with the
test data.
This commit is contained in:
Jiaxiang Chen
2019-06-06 01:37:06 -07:00
committed by max-kammerer
parent f17e9ba9fe
commit 0a2812f83b
13 changed files with 626 additions and 12 deletions
@@ -0,0 +1,17 @@
/*
* 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 org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.JVMConfigurationKeys
abstract class AbstractIrSteppingTest : AbstractSteppingTest() {
override fun updateConfiguration(configuration: CompilerConfiguration) {
super.updateConfiguration(configuration)
configuration.put(JVMConfigurationKeys.IR, true)
}
}
@@ -0,0 +1,259 @@
/*
* 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.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 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 java.io.File
import java.lang.IllegalStateException
import java.net.URLClassLoader
import kotlin.properties.Delegates
abstract class AbstractSteppingTest : CodegenTestCase() {
private companion object {
private const val DEBUG_ADDRESS = "127.0.0.1"
private const val MAIN_CLASS = "org.jetbrains.kotlin.test.clientserver.TestProcessServer"
private const val TEST_CLASS = "TestKt"
private const val BOX_METHOD = "box"
private const val LINENUMBER_PREFIX = "// LINENUMBERS"
}
private var proxyPort = 0
private var process: Process? = null
private var virtualMachine: VirtualMachine? = null
override fun setUp() {
val debugPort = startDebuggeeProcess()
virtualMachine = attachDebugger(debugPort)
setUpVM(virtualMachine!!)
val reader = process!!.inputStream.bufferedReader()
reader.readLine()
proxyPort = reader.readLine()
.split("port ")
.last()
.trim()
.toInt()
reader.close()
super.setUp()
}
open 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(
System.getProperty("java.class.path"),
PathUtil.getJarPathForClass(Delegates::class.java) // Add Kotlin runtime JAR
)
val command = arrayOf(
findJavaExecutable().absolutePath,
"-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)
})
}
override fun tearDown() {
try {
process!!.destroy()
} finally {
}
super.tearDown()
}
override fun doMultiFileTest(wholeFile: File, files: List<TestFile>) {
createEnvironmentWithMockJdkAndIdeaAnnotations(
ConfigurationKind.ALL, *listOfNotNull(
writeJavaFiles(
files
)
).toTypedArray()
)
val lineNumbers = 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 {
//process.destroy()
}
} 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<Event>()
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) {
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.*")
stepReq.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) {
break@vmLoop
}
}
else -> {
throw IllegalStateException("event not handled: $event")
}
}
}
}
val actualLineNumbers = loggedEvents
.map { event ->
"${(event as LocatableEvent).location().method()}:${event.location().lineNumber()}"
}
TestCase.assertEquals(expectedLineNumbers, actualLineNumbers.joinToString(" "))
}
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()) }
}
internal val OutputFile.internalName
get() = relativePath.substringBeforeLast(".class")
internal val OutputFile.qualifiedName
get() = internalName.replace('/', '.')
}