Debugger: Add 193 bunch for coroutines debugger

This commit is contained in:
Aleksandr Prokopyev
2019-10-15 17:11:05 +03:00
committed by Vladimir Ilmov
parent 1dc44b4000
commit 3ca5d2d64f
5 changed files with 135 additions and 143 deletions
@@ -0,0 +1,127 @@
/*
* 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.idea.debugger.coroutines
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.actions.ThreadDumpAction
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.execution.RunConfigurationExtension
import com.intellij.execution.configurations.DebuggingRunnerData
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.execution.ui.RunnerLayoutUi
import com.intellij.execution.ui.layout.PlaceInGrid
import com.intellij.execution.ui.layout.impl.RunnerContentUi
import com.intellij.execution.ui.layout.impl.RunnerLayoutUiImpl
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.content.ContentManagerAdapter
import com.intellij.ui.content.ContentManagerEvent
import com.intellij.util.messages.MessageBusConnection
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.XDebuggerManagerListener
import org.jetbrains.kotlin.psi.UserDataProperty
/**
* Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found
*/
@Suppress("IncompatibleAPI")
class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
private var Project.listenerCreated: Boolean? by UserDataProperty(Key.create("COROUTINES_DEBUG_TAB_CREATE_LISTENER"))
override fun isApplicableFor(configuration: RunConfigurationBase<*>): Boolean {
return Registry.`is`("kotlin.debugger.coroutines")
}
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
configuration: T,
params: JavaParameters,
runnerSettings: RunnerSettings?
) {
if (!Registry.`is`("kotlin.debugger.coroutines")) return
if (runnerSettings is DebuggingRunnerData
&& params.classPath != null
&& params.classPath.pathList.isNotEmpty()
) {
params.classPath.pathList.forEach {
if (!it.contains("kotlinx-coroutines-debug")) return@forEach
// if debug library is included into project, add agent which installs probes
params.vmParametersList?.add("-javaagent:$it")
params.vmParametersList?.add("-ea")
val project = (configuration as RunConfigurationBase<*>).project
// add listener to put coroutines tab into debugger tab
if (project.listenerCreated != true) { // prevent multiple listeners creation
val connection = project.messageBus.connect()
connection.subscribe(
XDebuggerManager.TOPIC, createListener(project, connection)
)
project.listenerCreated = true
return
}
}
}
}
private fun createListener(project: Project, connection: MessageBusConnection): XDebuggerManagerListener {
return object : XDebuggerManagerListener {
override fun processStarted(debugProcess: XDebugProcess) {
DebuggerInvocationUtil.swingInvokeLater(project) {
val session = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
val ui = session?.xDebugSession?.ui
if (ui != null)
registerCoroutinesPanel(ui, session)
}
}
override fun processStopped(debugProcess: XDebugProcess) {
connection.disconnect()
project.listenerCreated = false
}
}
}
/**
* Adds panel to XDebugSessionTab
*/
private fun registerCoroutinesPanel(ui: RunnerLayoutUi, session: DebuggerSession) {
val panel = CoroutinesPanel(session.project, session.contextManager)
val content = ui.createContent(
"CoroutinesContent", panel, "Coroutines", // TODO(design)
AllIcons.Debugger.ThreadGroup, null
)
content.isCloseable = false
ui.addContent(content, 0, PlaceInGrid.left, true)
ui.addListener(object : ContentManagerAdapter() {
override fun selectionChanged(event: ContentManagerEvent) {
if (event.content === content) {
if (content.isSelected) {
panel.setUpdateEnabled(true)
if (panel.isRefreshNeeded) {
panel.rebuildIfVisible(DebuggerSession.Event.CONTEXT)
}
} else {
panel.setUpdateEnabled(false)
}
}
}
}, content)
// add coroutine dump button: due to api problem left toolbar is copied, modified and reset to tab
val runnerContent = (ui.options as RunnerLayoutUiImpl).getData(RunnerContentUi.KEY.name) as RunnerContentUi
val modifiedActions = runnerContent.getActions(true)
val pos = modifiedActions.indexOfLast { it is ThreadDumpAction }
modifiedActions.add(pos + 1, ActionManager.getInstance().getAction("Kotlin.XDebugger.CoroutinesDump"))
ui.options.setLeftToolbar(DefaultActionGroup(modifiedActions), ActionPlaces.DEBUGGER_TOOLBAR)
}
}
@@ -41,13 +41,15 @@ object CoroutinesDebugProbesProxy {
@Synchronized
fun dumpCoroutines(context: ExecutionContext): Either<Throwable, List<CoroutineState>> {
try {
if (context.debugProcess.references == null) {
context.debugProcess.references = ProcessReferences(context)
var refs = context.debugProcess.references
if (refs == null) {
refs = ProcessReferences(context)
context.debugProcess.references = refs
}
val refs = context.debugProcess.references!! // already initialized if it was null
// get dump
val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as ObjectReference
val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as? ObjectReference
?: return Either.right(emptyList())
context.keepReference(infoList)
val size = (context.invokeMethod(infoList, refs.getSize, emptyList()) as IntegerValue).value()
@@ -132,7 +134,7 @@ object CoroutinesDebugProbesProxy {
mergedFrameList, refs.getElement,
listOf(context.vm.virtualMachine.mirrorOf(it))
) as ObjectReference
val clazz = (frame.getValue(refs.className) as StringReference).value()
val clazz = (frame.getValue(refs.className) as? StringReference)?.value()
list.add(
0, // add in the beginning
StackTraceElement(
@@ -164,10 +164,6 @@ class SuspendStackFrameDescriptor(
override fun getName(): String {
return frame.methodName
}
override fun getIcon(): Icon {
return IconLoader.getIcon("org/jetbrains/kotlin/idea/icons/suspendCall.${IconExtensionChooser.iconExtension()}")
}
}
class AsyncStackFrameDescriptor(val state: CoroutineState, val frame: StackFrameItem, proxy: StackFrameProxyImpl) :
@@ -1,87 +0,0 @@
/*
* 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.idea.debugger.coroutines
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.jarRepository.JarRepositoryManager
import com.intellij.jarRepository.RemoteRepositoryDescription
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.idea.maven.aether.ArtifactKind
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerTestBase
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import java.io.File
abstract class AbstractCoroutineDumpTest : KotlinDebuggerTestBase() {
protected fun doTest(path: String) {
val fileText = FileUtil.loadFile(File(path))
configureSettings(fileText)
createAdditionalBreakpoints(fileText)
createDebugProcess(path)
doOnBreakpoint {
val evalContext = EvaluationContextImpl(this, frameProxy)
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
try {
if (either.isRight)
try {
val states = either.get()
print(stringDump(states), ProcessOutputTypes.SYSTEM)
} catch (ignored: Throwable) {
}
else
throw AssertionError("Dump failed", either.left)
} finally {
resume(this)
}
}
doOnBreakpoint {
val evalContext = EvaluationContextImpl(this, frameProxy)
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
try {
if (either.isRight)
try {
val states = either.get()
print(stringDump(states), ProcessOutputTypes.SYSTEM)
} catch (ignored: Throwable) {
}
else
throw AssertionError("Dump failed", either.left)
} finally {
resume(this)
}
}
}
private fun stringDump(states: List<CoroutineState>) = buildString {
states.forEach {
appendln("\"${it.name}\", state: ${it.state}")
}
}
override fun createJavaParameters(mainClass: String?): JavaParameters {
val description = JpsMavenRepositoryLibraryDescriptor("org.jetbrains.kotlinx", "kotlinx-coroutines-debug", "1.3.0")
val debugJar = JarRepositoryManager.loadDependenciesSync(
project, description, setOf(ArtifactKind.ARTIFACT),
RemoteRepositoryDescription.DEFAULT_REPOSITORIES, null
) ?: throw AssertionError("Debug Dependency is not found")
val params = super.createJavaParameters(mainClass)
for (jar in debugJar) {
params.classPath.add(jar.file.presentableUrl)
if (jar.file.name.contains("kotlinx-coroutines-debug"))
params.vmParametersList.add("-javaagent:${jar.file.presentableUrl}")
}
return params
}
}
@@ -1,46 +0,0 @@
/*
* 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.idea.debugger.coroutines;
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("idea/testData/debugger/tinyApp/src/coroutines")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class CoroutineDumpTestGenerated extends AbstractCoroutineDumpTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInCoroutines() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/debugger/tinyApp/src/coroutines"), Pattern.compile("^(.+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("noCoroutines.kt")
public void testNoCoroutines() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt");
}
@TestMetadata("threeCoroutines.kt")
public void testThreeCoroutines() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt");
}
@TestMetadata("twoDumps.kt")
public void testTwoDumps() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt");
}
}