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
+16
View File
@@ -0,0 +1,16 @@
//FILE: test.kt
fun cond() = false
fun box() {
if (cond())
cond()
else
false
}
// LINENUMBERS
// TestKt.box():5
// TestKt.cond():2
// TestKt.box():5
// TestKt.box():8
// TestKt.box():9
+36
View File
@@ -0,0 +1,36 @@
//FILE: test.kt
fun box() {
val k = if (getA()
&& getB()
&& getC()
&& getD()) {
true
} else {
false
}
}
fun getA() = true
fun getB() = true
fun getC() = false
fun getD() = true
// IGNORE_BACKEND: JVM_IR
// IR backend is missing a line number 3 in box() after stepping on line 9 for getting the result of false
// LINENUMBERS
// TestKt.box():3
// TestKt.getA():13
// TestKt.box():3
// TestKt.box():4
// TestKt.getB():15
// TestKt.box():4
// TestKt.box():5
// TestKt.getC():17
// TestKt.box():5
// TestKt.box():9
// TestKt.box():3
// TestKt.box():11
@@ -0,0 +1,38 @@
//FILE: foo.kt
import bar
fun foo(x: Int): Int {
if (x >= 0) { // 4
return x // 5
}
return bar(x) // 7
}
//FILE: test.kt
import foo
fun box() {
foo(-3) //4
}
fun bar(x: Int) =
if (x < 0) { //8
foo(0)
} else { // 10
foo(x)
}
// IGNORE_BACKEND: JVM_IR
// IR backend has bar().12 replaced as bar().8 for returning bar() function result.
// LINENUMBERS
// TestKt.box():4
// FooKt.foo(int):4
// FooKt.foo(int):7
// TestKt.bar(int):8
// TestKt.bar(int):9
// FooKt.foo(int):4
// FooKt.foo(int):5
// TestKt.bar(int):9
// TestKt.bar(int):12
// FooKt.foo(int):7
// TestKt.box():4
// TestKt.box():5
+26
View File
@@ -0,0 +1,26 @@
//FILE: test.kt
fun box(): Int {
if (
getB() ==
getA())
return 0
return getB()
}
fun getA() = 3
inline fun getB(): Int {
return 1
}
// IGNORE_BACKEND: JVM_IR
// old backend is missing a line number after return from inline function call, actually IR backend results seems more right.
// LINENUMBERS
// TestKt.box():4
// TestKt.box():13
// TestKt.box():5
// TestKt.getA():10
// TestKt.box():5
// TestKt.box():7
// TestKt.box():13
+26
View File
@@ -0,0 +1,26 @@
//FILE: test.kt
fun box() {
val n = 3
val k = foo(n)
}
fun foo(n :Int ) : Int {
if (n == 1 || n == 0) {
return 1
}
return foo(n-1) * n
}
// LINENUMBERS
// TestKt.box():3
// TestKt.box():4
// TestKt.foo(int):8
// TestKt.foo(int):11
// TestKt.foo(int):8
// TestKt.foo(int):11
// TestKt.foo(int):8
// TestKt.foo(int):9
// TestKt.foo(int):11
// TestKt.foo(int):11
// TestKt.box():4
// TestKt.box():5
+29
View File
@@ -0,0 +1,29 @@
//FILE: test.kt
fun box() {
val a = 1
val b = 2
try {
throwIfLess(a, b)
} catch (e: java.lang.Exception) {
throwIfLess(a, b)
}
throwIfLess(b,a)
}
fun throwIfLess(a: Int, b: Int) {
if (a<b)
throw java.lang.IllegalStateException()
}
// IGNORE_BACKEND: JVM_IR
// after throwing exception in try block, IR backend is returning a line number at line 6 instead of line 7 where catch statement is in.
// LINENUMBERS
// TestKt.box():3
// TestKt.box():4
// TestKt.box():5
// TestKt.box():6
// TestKt.throwIfLess(int, int):14
// TestKt.throwIfLess(int, int):15
// TestKt.box():7
// TestKt.box():8
// TestKt.throwIfLess(int, int):14
// TestKt.throwIfLess(int, int):15
@@ -49,6 +49,8 @@ fun getBoxMethodOrNull(aClass: Class<*>): Method? {
//Use only JDK 1.6 compatible api
object TestProcessServer {
const val DEBUG_TEST = "--debugTest"
private val executor = Executors.newFixedThreadPool(1)!!
@Volatile
@@ -63,13 +65,21 @@ object TestProcessServer {
private lateinit var serverSocket: ServerSocket
private var suppressOutput = false
private var allocatePort = false
@JvmStatic
fun main(args: Array<String>) {
val portNumber = args[0].toInt()
println("Starting server on port $portNumber...")
if (args[0] == DEBUG_TEST) {
suppressOutput = true
allocatePort = true
}
val portNumber = if (allocatePort) 0 else args[0].toInt()
println("Starting server on port $portNumber...")
val serverSocket = ServerSocket(portNumber)
sheduleShutdownProcess()
println("...server started on port ${serverSocket.localPort}")
scheduleShutdownProcess()
try {
while (true) {
@@ -78,7 +88,7 @@ object TestProcessServer {
val clientSocket = serverSocket.accept()
isProcessingTask = true
println("Socket established...")
executor.execute(ServerTest(clientSocket))
executor.execute(ServerTest(clientSocket, suppressOutput))
}
}
finally {
@@ -89,7 +99,7 @@ object TestProcessServer {
}
}
private fun sheduleShutdownProcess() {
private fun scheduleShutdownProcess() {
handler = scheduler.scheduleAtFixedRate({
if (!isProcessingTask && (System.currentTimeMillis() - lastTime) >= 60 * 1000 /*60 sec*/) {
println("Stopping server...")
@@ -97,15 +107,21 @@ object TestProcessServer {
}
}, 60, 60, TimeUnit.SECONDS)
}
private fun printlnTest(text: String) {
if (!suppressOutput) {
println(text)
}
}
}
private class ServerTest(val clientSocket: Socket) : Runnable {
private class ServerTest(val clientSocket: Socket, val suppressOutput: Boolean) : Runnable {
private lateinit var className: String
private lateinit var testMethod: String
override fun run() {
val input = ObjectInputStream(clientSocket.getInputStream())
val output = ObjectOutputStream(clientSocket.getOutputStream())
val output = if (suppressOutput) null else ObjectOutputStream(clientSocket.getOutputStream())
try {
var message = input.readObject() as MessageHeader
assert(message == MessageHeader.NEW_TEST, { "New test marker missed, but $message received" })
@@ -119,13 +135,13 @@ private class ServerTest(val clientSocket: Socket) : Runnable {
val classPath = input.readObject() as Array<URL>
val result = executeTest(URLClassLoader(classPath, JDK_EXT_JARS_CLASS_LOADER))
output.writeObject(MessageHeader.RESULT)
output.writeObject(result)
output?.writeObject(MessageHeader.RESULT)
output?.writeObject(result)
} catch (e: Throwable) {
output.writeObject(MessageHeader.ERROR)
output.writeObject(e)
output?.writeObject(MessageHeader.ERROR)
output?.writeObject(e)
} finally {
output.close()
output?.close()
input.close()
clientSocket.close()
}
@@ -54,6 +54,25 @@ class TestProxy(val serverPort: Int, val testClass: String, val classPath: List<
}
}
fun runTestNoOutput(): String {
return Socket("localhost", serverPort).use { clientSocket ->
val output = ObjectOutputStream(clientSocket.getOutputStream())
try {
output.writeObject(MessageHeader.NEW_TEST)
output.writeObject(testClass)
output.writeObject("box")
output.writeObject(MessageHeader.CLASS_PATH)
//filter out jdk libs
output.writeObject(filterOutJdkJars(classPath).toTypedArray())
return "OK"
} finally {
output.close()
}
}
}
fun filterOutJdkJars(classPath: List<URL>): List<URL> {
val javaHome = System.getProperty("java.home")
val javaFolder = File(javaHome)
@@ -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('/', '.')
}
@@ -0,0 +1,61 @@
/*
* 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.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TargetBackend;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("compiler/testData/debug/stepping")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class IrSteppingTestGenerated extends AbstractIrSteppingTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.JVM_IR, testDataFilePath);
}
public void testAllFilesPresentInStepping() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/debug/stepping"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JVM_IR, true);
}
@TestMetadata("conjunction.kt")
public void testConjunction() throws Exception {
runTest("compiler/testData/debug/stepping/conjunction.kt");
}
@TestMetadata("functionInAnotherFile.kt")
public void testFunctionInAnotherFile() throws Exception {
runTest("compiler/testData/debug/stepping/functionInAnotherFile.kt");
}
@TestMetadata("if.kt")
public void testIf() throws Exception {
runTest("compiler/testData/debug/stepping/if.kt");
}
@TestMetadata("IfTrueThenFalse.kt")
public void testIfTrueThenFalse() throws Exception {
runTest("compiler/testData/debug/stepping/IfTrueThenFalse.kt");
}
@TestMetadata("recursion.kt")
public void testRecursion() throws Exception {
runTest("compiler/testData/debug/stepping/recursion.kt");
}
@TestMetadata("throwException.kt")
public void testThrowException() throws Exception {
runTest("compiler/testData/debug/stepping/throwException.kt");
}
}
@@ -0,0 +1,61 @@
/*
* 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.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TargetBackend;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("compiler/testData/debug/stepping")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class SteppingTestGenerated extends AbstractSteppingTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.JVM, testDataFilePath);
}
public void testAllFilesPresentInStepping() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/debug/stepping"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JVM, true);
}
@TestMetadata("conjunction.kt")
public void testConjunction() throws Exception {
runTest("compiler/testData/debug/stepping/conjunction.kt");
}
@TestMetadata("functionInAnotherFile.kt")
public void testFunctionInAnotherFile() throws Exception {
runTest("compiler/testData/debug/stepping/functionInAnotherFile.kt");
}
@TestMetadata("if.kt")
public void testIf() throws Exception {
runTest("compiler/testData/debug/stepping/if.kt");
}
@TestMetadata("IfTrueThenFalse.kt")
public void testIfTrueThenFalse() throws Exception {
runTest("compiler/testData/debug/stepping/IfTrueThenFalse.kt");
}
@TestMetadata("recursion.kt")
public void testRecursion() throws Exception {
runTest("compiler/testData/debug/stepping/recursion.kt");
}
@TestMetadata("throwException.kt")
public void testThrowException() throws Exception {
runTest("compiler/testData/debug/stepping/throwException.kt");
}
}
@@ -14,6 +14,8 @@ import org.jetbrains.kotlin.checkers.*
import org.jetbrains.kotlin.checkers.javac.*
import org.jetbrains.kotlin.cli.AbstractCliTest
import org.jetbrains.kotlin.codegen.*
import org.jetbrains.kotlin.codegen.debugInformation.AbstractIrSteppingTest
import org.jetbrains.kotlin.codegen.debugInformation.AbstractSteppingTest
import org.jetbrains.kotlin.codegen.defaultConstructor.AbstractDefaultArgumentsReflectionTest
import org.jetbrains.kotlin.codegen.flags.AbstractWriteFlagsTest
import org.jetbrains.kotlin.codegen.ir.*
@@ -342,6 +344,10 @@ fun main(args: Array<String>) {
model("lineNumber")
}
testClass<AbstractSteppingTest> {
model("debug/stepping", targetBackend = TargetBackend.JVM)
}
testClass<AbstractLocalClassProtoTest> {
model("serialization/local")
}
@@ -419,6 +425,10 @@ fun main(args: Array<String>) {
model("lineNumber", targetBackend = TargetBackend.JVM_IR)
}
testClass<AbstractIrSteppingTest> {
model("debug/stepping", targetBackend = TargetBackend.JVM_IR)
}
testClass<AbstractIrBlackBoxInlineCodegenTest> {
model("codegen/boxInline", targetBackend = TargetBackend.JVM_IR)
}