blob: c63ff7ca864acbb4f04670b49e426ee5e7453f78 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.compiler.plugins.kotlin.debug
import androidx.compose.compiler.plugins.kotlin.AbstractCodegenTest
import androidx.compose.compiler.plugins.kotlin.CodegenTestFiles
import androidx.compose.compiler.plugins.kotlin.debug.clientserver.TestProxy
import androidx.compose.compiler.plugins.kotlin.tmpDir
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.VirtualMachine
import com.sun.jdi.event.BreakpointEvent
import com.sun.jdi.event.ClassPrepareEvent
import com.sun.jdi.event.LocatableEvent
import com.sun.jdi.event.MethodEntryEvent
import com.sun.jdi.event.MethodExitEvent
import com.sun.jdi.event.StepEvent
import com.sun.jdi.event.VMDeathEvent
import com.sun.jdi.event.VMDisconnectEvent
import com.sun.jdi.event.VMStartEvent
import com.sun.jdi.request.EventRequest.SUSPEND_ALL
import com.sun.jdi.request.MethodEntryRequest
import com.sun.jdi.request.MethodExitRequest
import com.sun.jdi.request.StepRequest
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.backend.common.output.SimpleOutputFileCollection
import org.jetbrains.kotlin.cli.common.output.writeAllTo
import org.jetbrains.kotlin.codegen.GeneratedClassLoader
import org.jetbrains.kotlin.psi.KtFile
import java.net.URL
import java.net.URLClassLoader
private const val RUNNER_CLASS = "RunnerKt"
private const val MAIN_METHOD = "main"
private const val CONTENT_METHOD = "content"
private const val TEST_CLASS = "TestKt"
abstract class AbstractDebuggerTest : AbstractCodegenTest() {
private lateinit var virtualMachine: VirtualMachine
private var proxyPort: Int = -1
private lateinit var methodEntryRequest: MethodEntryRequest
private lateinit var methodExitRequest: MethodExitRequest
fun initialize(vm: VirtualMachine, port: Int) {
virtualMachine = vm
proxyPort = port
}
override fun setUp() {
super.setUp()
if (proxyPort == -1) error("initialize method must be called on AbstractDebuggerTest")
createMethodEventsForTestClass()
}
override fun tearDown() {
super.tearDown()
virtualMachine.eventRequestManager()
.deleteEventRequests(listOf(methodEntryRequest, methodExitRequest))
}
private fun createMethodEventsForTestClass() {
val manager = virtualMachine.eventRequestManager()
methodEntryRequest = manager.createMethodEntryRequest()
methodEntryRequest.addClassFilter(TEST_CLASS)
methodEntryRequest.setSuspendPolicy(SUSPEND_ALL)
methodEntryRequest.enable()
methodExitRequest = manager.createMethodExitRequest()
methodExitRequest.addClassFilter(TEST_CLASS)
methodExitRequest.setSuspendPolicy(SUSPEND_ALL)
methodExitRequest.enable()
}
private fun invokeRunnerMainInSeparateProcess(
classLoader: URLClassLoader,
port: Int
) {
val classPath = classLoader.extractUrls().toMutableList()
if (classLoader is GeneratedClassLoader) {
val outDir = tmpDir("${this::class.simpleName}_${this.name}")
val currentOutput = SimpleOutputFileCollection(classLoader.allGeneratedFiles)
currentOutput.writeAllTo(outDir)
classPath.add(0, outDir.toURI().toURL())
}
TestProxy(port, RUNNER_CLASS, MAIN_METHOD, classPath).runTest()
}
fun collectDebugEvents(@Language("kotlin") source: String): List<LocatableEvent> {
val files = mutableListOf<KtFile>()
files.addAll(helperFiles())
files.add(sourceFile("Runner.kt", RUNNER_SOURCES))
files.add(sourceFile("Test.kt", source))
myFiles = CodegenTestFiles.create(files)
return doTest()
}
private fun doTest(): List<LocatableEvent> {
val classLoader = createClassLoader()
val testClass = classLoader.loadClass(TEST_CLASS)
assert(testClass.declaredMethods.any { it.name == CONTENT_METHOD }) {
"Test method $CONTENT_METHOD not present on test class $TEST_CLASS"
}
if (virtualMachine.allThreads().any { it.isSuspended }) {
virtualMachine.resume()
}
invokeRunnerMainInSeparateProcess(classLoader, proxyPort)
val manager = virtualMachine.eventRequestManager()
val loggedItems = mutableListOf<LocatableEvent>()
var inContentMethod = false
vmLoop@
while (true) {
val eventSet = virtualMachine.eventQueue().remove(1000) ?: continue
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 (!inContentMethod &&
event.location().method().name() == CONTENT_METHOD
) {
if (manager.stepRequests().isEmpty()) {
// Create line stepping request to get all normal line steps starting now.
val stepReq = manager.createStepRequest(
event.thread(),
StepRequest.STEP_LINE,
StepRequest.STEP_INTO
)
stepReq.setSuspendPolicy(SUSPEND_ALL)
stepReq.addClassExclusionFilter("java.*")
stepReq.addClassExclusionFilter("sun.*")
stepReq.addClassExclusionFilter("kotlin.*")
stepReq.addClassExclusionFilter("kotlinx.*")
stepReq.addClassExclusionFilter("androidx.compose.runtime.*")
stepReq.addClassExclusionFilter("jdk.internal.*")
// Create class prepare request to be able to set breakpoints on class initializer lines.
// There are no line stepping events for class initializers, so we depend on breakpoints.
val prepareReq = manager.createClassPrepareRequest()
prepareReq.setSuspendPolicy(SUSPEND_ALL)
prepareReq.addClassExclusionFilter("java.*")
prepareReq.addClassExclusionFilter("sun.*")
prepareReq.addClassExclusionFilter("kotlinx.*")
prepareReq.addClassExclusionFilter("androidx.compose.runtime.*")
prepareReq.addClassExclusionFilter("jdk.internal.*")
}
manager.stepRequests().map { it.enable() }
manager.classPrepareRequests().map { it.enable() }
inContentMethod = true
loggedItems.add(event)
}
}
is StepEvent -> {
// Handle the case where an Exception causing program to exit without MethodExitEvent.
if (inContentMethod && event.location().method().name() == "run") {
manager.stepRequests().map { it.disable() }
manager.classPrepareRequests().map { it.disable() }
manager.breakpointRequests().map { it.disable() }
break@vmLoop
}
if (inContentMethod) {
loggedItems.add(event)
}
}
is MethodExitEvent -> {
if (event.location().method().name() == CONTENT_METHOD) {
manager.stepRequests().map { it.disable() }
manager.classPrepareRequests().map { it.disable() }
manager.breakpointRequests().map { it.disable() }
break@vmLoop
}
}
is ClassPrepareEvent -> {
if (inContentMethod) {
val initializer =
event.referenceType().methods().find { it.isStaticInitializer }
try {
initializer?.allLineLocations()?.forEach {
manager.createBreakpointRequest(it).enable()
}
} catch (e: AbsentInformationException) {
// If there is no line information, do not set breakpoints.
}
}
}
is BreakpointEvent -> {
if (inContentMethod) {
loggedItems.add(event)
}
}
else -> {
throw IllegalStateException("event not handled: $event")
}
}
}
eventSet.resume()
}
virtualMachine.resume()
return loggedItems
}
}
private fun ClassLoader?.extractUrls(): List<URL> {
return (this as? URLClassLoader)?.let {
it.urLs.toList() + it.parent.extractUrls()
} ?: emptyList()
}
@Language("kotlin")
private val RUNNER_SOURCES = """
import androidx.compose.runtime.*
import kotlinx.coroutines.*
fun main() {
val mainScope = CoroutineScope(
NonCancellable + Dispatchers.Main
)
val recomposer = Recomposer(mainScope.coroutineContext)
Composition(EmptyApplier(), recomposer).setContent { content() }
}
private class EmptyApplier : Applier<Unit> {
override val current: Unit = Unit
override fun down(node: Unit) {}
override fun up() {}
override fun insertTopDown(index: Int, instance: Unit) {
error("Unexpected")
}
override fun insertBottomUp(index: Int, instance: Unit) {
error("Unexpected")
}
override fun remove(index: Int, count: Int) {
error("Unexpected")
}
override fun move(from: Int, to: Int, count: Int) {
error("Unexpected")
}
override fun clear() {}
}
""".trimIndent()