refactor stepping test to extract common logic for other debug information test
This commit is contained in:
committed by
Ilmir Usmanov
parent
dec7f51902
commit
6b73ef4b0b
+233
@@ -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<TestFile>) {
|
||||
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<Any>()
|
||||
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<Any>, event: Event)
|
||||
|
||||
abstract fun checkResult(wholeFile: File, loggedItems: List<Any>)
|
||||
}
|
||||
+16
-224
@@ -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<Any>, event: Event) {
|
||||
assert(event is LocatableEvent)
|
||||
loggedItems.add(event)
|
||||
}
|
||||
|
||||
@After
|
||||
public override fun tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
override fun doMultiFileTest(wholeFile: File, files: List<TestFile>) {
|
||||
createEnvironmentWithMockJdkAndIdeaAnnotations(
|
||||
ConfigurationKind.ALL, *listOfNotNull(
|
||||
writeJavaFiles(
|
||||
files
|
||||
)
|
||||
).toTypedArray()
|
||||
)
|
||||
|
||||
val lineNumbers = wholeFile
|
||||
override fun checkResult(wholeFile: File, loggedItems: List<Any>) {
|
||||
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<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) {
|
||||
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('/', '.')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user