mirror of
https://github.com/jlengrand/kotlin.git
synced 2026-04-27 15:52:52 +00:00
Debugger: Coroutines stack frames with variables & coroutine dumps
This commit is contained in:
committed by
Vladimir Ilmov
parent
c459b2ca6e
commit
5975251a32
@@ -55,6 +55,8 @@ import org.jetbrains.kotlin.idea.conversion.copy.AbstractLiteralKotlinToKotlinCo
|
||||
import org.jetbrains.kotlin.idea.conversion.copy.AbstractLiteralTextToKotlinCopyPasteTest
|
||||
import org.jetbrains.kotlin.idea.conversion.copy.AbstractTextJavaToKotlinCopyPasteConversionTest
|
||||
import org.jetbrains.kotlin.idea.coverage.AbstractKotlinCoverageOutputFilesTest
|
||||
import org.jetbrains.kotlin.idea.debugger.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.AbstractCoroutineDumpTest
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.*
|
||||
import org.jetbrains.kotlin.idea.debugger.test.sequence.exec.AbstractSequenceTraceTestCase
|
||||
import org.jetbrains.kotlin.idea.debugger.test.*
|
||||
|
||||
@@ -140,7 +140,7 @@ class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceP
|
||||
return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber)
|
||||
}
|
||||
|
||||
private fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List<XNamedValue>? {
|
||||
fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List<XNamedValue>? {
|
||||
val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName(
|
||||
"getSpilledVariableFieldMapping",
|
||||
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;"
|
||||
@@ -188,7 +188,7 @@ class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceP
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncStackTraceContext(
|
||||
class AsyncStackTraceContext(
|
||||
val context: ExecutionContext,
|
||||
val method: Method,
|
||||
val debugMetadataKtType: ClassType
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.DebuggerManagerEx
|
||||
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
||||
import com.intellij.debugger.impl.DebuggerUtilsEx
|
||||
import com.intellij.execution.filters.ExceptionFilters
|
||||
import com.intellij.execution.filters.TextConsoleBuilderFactory
|
||||
import com.intellij.execution.ui.RunnerLayoutUi
|
||||
import com.intellij.execution.ui.layout.impl.RunnerContentUi
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.MessageType
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.util.text.DateFormatUtil
|
||||
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.createExecutionContext
|
||||
|
||||
@Suppress("ComponentNotRegistered")
|
||||
class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
||||
private val logger = Logger.getInstance(this::class.java)
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||
val session = context.debuggerSession
|
||||
if (session != null && session.isAttached) {
|
||||
val process = context.debugProcess ?: return
|
||||
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||
override fun contextAction() {
|
||||
val execContext = context.createExecutionContext() ?: return
|
||||
val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
||||
if (states.isLeft) {
|
||||
logger.warn(states.left)
|
||||
XDebuggerManagerImpl.NOTIFICATION_GROUP
|
||||
.createNotification(
|
||||
"Coroutine dump failed. See log",
|
||||
MessageType.ERROR
|
||||
).notify(project)
|
||||
return
|
||||
}
|
||||
val f = fun() {
|
||||
addCoroutineDump(
|
||||
project,
|
||||
states.get(),
|
||||
session.xDebugSession?.ui ?: return,
|
||||
session.searchScope
|
||||
)
|
||||
}
|
||||
ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analog of [DebuggerUtilsEx.addThreadDump].
|
||||
*/
|
||||
fun addCoroutineDump(project: Project, coroutines: List<CoroutineState>, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) {
|
||||
val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project)
|
||||
consoleBuilder.filters(ExceptionFilters.getFilters(searchScope))
|
||||
val consoleView = consoleBuilder.console
|
||||
val toolbarActions = DefaultActionGroup()
|
||||
consoleView.allowHeavyFilters()
|
||||
val panel = CoroutineDumpPanel(project, consoleView, toolbarActions, coroutines)
|
||||
|
||||
val id = "DumpKt " + DateFormatUtil.formatTimeWithSeconds(System.currentTimeMillis())
|
||||
val content = ui.createContent(id, panel, id, null, null).apply {
|
||||
putUserData(RunnerContentUi.LIGHTWEIGHT_CONTENT_MARKER, true)
|
||||
isCloseable = true
|
||||
description = "Coroutine Dump"
|
||||
}
|
||||
ui.addContent(content)
|
||||
ui.selectAndFocus(content, true, true)
|
||||
Disposer.register(content, consoleView)
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val presentation = e.presentation
|
||||
val project = e.project
|
||||
if (project == null) {
|
||||
presentation.isEnabled = false
|
||||
presentation.isVisible = false
|
||||
return
|
||||
}
|
||||
// cannot be called when no SuspendContext
|
||||
if (DebuggerManagerEx.getInstanceEx(project).context.suspendContext == null) {
|
||||
presentation.isEnabled = false
|
||||
return
|
||||
}
|
||||
val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
|
||||
presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && Registry.`is`("kotlin.debugger" + ".coroutines")
|
||||
presentation.isVisible = presentation.isEnabled
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* 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.codeInsight.highlighting.HighlightManager
|
||||
import com.intellij.execution.ui.ConsoleView
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.icons.AllIcons.Debugger.ThreadStates.Daemon_sign
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.ide.ExporterToTextFile
|
||||
import com.intellij.ide.ui.UISettings
|
||||
import com.intellij.notification.NotificationGroup
|
||||
import com.intellij.openapi.actionSystem.*
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.colors.EditorColors
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.ide.CopyPasteManager
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.MessageType
|
||||
import com.intellij.openapi.ui.Splitter
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.openapi.wm.ToolWindowId
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.unscramble.AnalyzeStacktraceUtil
|
||||
import com.intellij.util.PlatformIcons
|
||||
import com.intellij.util.ui.EmptyIcon
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.io.File
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
|
||||
/**
|
||||
* Panel with dump of coroutines
|
||||
*/
|
||||
class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List<CoroutineState>) :
|
||||
JPanel(BorderLayout()), DataProvider {
|
||||
private var exporterToTextFile: ExporterToTextFile
|
||||
private var mergedDump = ArrayList<CoroutineState>()
|
||||
val filterField = SearchTextField()
|
||||
val filterPanel = JPanel(BorderLayout())
|
||||
private val coroutinesList = JBList(DefaultListModel<Any>())
|
||||
|
||||
init {
|
||||
mergedDump.addAll(dump)
|
||||
|
||||
filterField.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
updateCoroutinesList()
|
||||
}
|
||||
})
|
||||
|
||||
filterPanel.apply {
|
||||
add(JLabel("Filter:"), BorderLayout.WEST)
|
||||
add(filterField)
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
coroutinesList.apply {
|
||||
cellRenderer = CoroutineListCellRenderer()
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
addListSelectionListener {
|
||||
val index = selectedIndex
|
||||
if (index >= 0) {
|
||||
val selection = model.getElementAt(index) as CoroutineState
|
||||
AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace)
|
||||
} else {
|
||||
AnalyzeStacktraceUtil.printStacktrace(consoleView, "")
|
||||
}
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
exporterToTextFile = createToFileExporter(project, dump)
|
||||
|
||||
val filterAction = FilterAction().apply {
|
||||
registerCustomShortcutSet(
|
||||
ActionManager.getInstance().getAction(IdeActions.ACTION_FIND).shortcutSet,
|
||||
coroutinesList
|
||||
)
|
||||
}
|
||||
toolbarActions.apply {
|
||||
add(filterAction)
|
||||
add(CopyToClipboardAction(dump, project))
|
||||
add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE))
|
||||
add(MergeStacktracesAction())
|
||||
}
|
||||
add(
|
||||
ActionManager.getInstance()
|
||||
.createActionToolbar("CoroutinesDump", toolbarActions, false).component,
|
||||
BorderLayout.WEST
|
||||
)
|
||||
|
||||
val leftPanel = JPanel(BorderLayout()).apply {
|
||||
add(filterPanel, BorderLayout.NORTH)
|
||||
add(ScrollPaneFactory.createScrollPane(coroutinesList, SideBorder.LEFT or SideBorder.RIGHT), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
val splitter = Splitter(false, 0.3f).apply {
|
||||
firstComponent = leftPanel
|
||||
secondComponent = consoleView.component
|
||||
}
|
||||
add(splitter, BorderLayout.CENTER)
|
||||
|
||||
ListSpeedSearch(coroutinesList).comparator = SpeedSearchComparator(false, true)
|
||||
|
||||
updateCoroutinesList()
|
||||
|
||||
val editor = CommonDataKeys.EDITOR.getData(
|
||||
DataManager.getInstance()
|
||||
.getDataContext(consoleView.preferredFocusableComponent)
|
||||
)
|
||||
editor?.document?.addDocumentListener(object : DocumentListener {
|
||||
override fun documentChanged(e: com.intellij.openapi.editor.event.DocumentEvent) {
|
||||
val filter = filterField.text
|
||||
if (StringUtil.isNotEmpty(filter)) {
|
||||
highlightOccurrences(filter, project, editor)
|
||||
}
|
||||
}
|
||||
}, consoleView)
|
||||
}
|
||||
|
||||
private fun updateCoroutinesList() {
|
||||
val text = if (filterPanel.isVisible) filterField.text else ""
|
||||
val selection = coroutinesList.selectedValue
|
||||
val model = coroutinesList.model as DefaultListModel<Any>
|
||||
model.clear()
|
||||
var selectedIndex = 0
|
||||
var index = 0
|
||||
val states = if (UISettings.instance.state.mergeEqualStackTraces) mergedDump else dump
|
||||
for (state in states) {
|
||||
if (StringUtil.containsIgnoreCase(state.stringStackTrace, text) || StringUtil.containsIgnoreCase(state.name, text)) {
|
||||
|
||||
model.addElement(state)
|
||||
if (selection === state) {
|
||||
selectedIndex = index
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
if (!model.isEmpty) {
|
||||
coroutinesList.selectedIndex = selectedIndex
|
||||
}
|
||||
coroutinesList.revalidate()
|
||||
coroutinesList.repaint()
|
||||
}
|
||||
|
||||
internal fun highlightOccurrences(filter: String, project: Project, editor: Editor) {
|
||||
val highlightManager = HighlightManager.getInstance(project)
|
||||
val colorManager = EditorColorsManager.getInstance()
|
||||
val attributes = colorManager.globalScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES)
|
||||
val documentText = editor.document.text
|
||||
var i = -1
|
||||
while (true) {
|
||||
val nextOccurrence = StringUtil.indexOfIgnoreCase(documentText, filter, i + 1)
|
||||
if (nextOccurrence < 0) {
|
||||
break
|
||||
}
|
||||
i = nextOccurrence
|
||||
highlightManager.addOccurrenceHighlight(
|
||||
editor, i, i + filter.length, attributes,
|
||||
HighlightManager.HIDE_BY_TEXT_CHANGE, null, null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getData(dataId: String): Any? = if (PlatformDataKeys.EXPORTER_TO_TEXT_FILE.`is`(dataId)) exporterToTextFile else null
|
||||
|
||||
private fun getCoroutineStateIcon(state: CoroutineState): Icon {
|
||||
return when (state.state) {
|
||||
CoroutineState.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
|
||||
CoroutineState.State.SUSPENDED -> AllIcons.Actions.Pause
|
||||
else -> EmptyIcon.create(6)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAttributes(state: CoroutineState): SimpleTextAttributes {
|
||||
return when {
|
||||
state.isSuspended -> SimpleTextAttributes.GRAY_ATTRIBUTES
|
||||
state.isEmptyStackTrace -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
|
||||
else -> SimpleTextAttributes.REGULAR_ATTRIBUTES
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CoroutineListCellRenderer : ColoredListCellRenderer<Any>() {
|
||||
|
||||
override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) {
|
||||
val state = value as CoroutineState
|
||||
icon = getCoroutineStateIcon(state)
|
||||
val attrs = getAttributes(state)
|
||||
append(state.name + " (", attrs)
|
||||
var detail: String? = state.state.name
|
||||
if (detail == null) {
|
||||
detail = state.state.name
|
||||
}
|
||||
if (detail.length > 30) {
|
||||
detail = detail.substring(0, 30) + "..."
|
||||
}
|
||||
append(detail, attrs)
|
||||
append(")", attrs)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FilterAction :
|
||||
ToggleAction("Filter", "Show only threads containing a specific string", AllIcons.General.Filter),
|
||||
DumbAware {
|
||||
|
||||
override fun isSelected(e: AnActionEvent): Boolean {
|
||||
return filterPanel.isVisible
|
||||
}
|
||||
|
||||
override fun setSelected(e: AnActionEvent, state: Boolean) {
|
||||
filterPanel.isVisible = state
|
||||
if (state) {
|
||||
IdeFocusManager.getInstance(AnAction.getEventProject(e)).requestFocus(filterField, true)
|
||||
filterField.selectText()
|
||||
}
|
||||
updateCoroutinesList()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private inner class MergeStacktracesAction :
|
||||
ToggleAction(
|
||||
"Merge Identical Stacktraces",
|
||||
"Group coroutines with identical stacktraces",
|
||||
AllIcons.General.CollapseAll
|
||||
),
|
||||
DumbAware {
|
||||
|
||||
override fun isSelected(e: AnActionEvent): Boolean {
|
||||
return UISettings.instance.state.mergeEqualStackTraces
|
||||
}
|
||||
|
||||
override fun setSelected(e: AnActionEvent, state: Boolean) {
|
||||
UISettings.instance.state.mergeEqualStackTraces = state
|
||||
updateCoroutinesList()
|
||||
}
|
||||
}
|
||||
|
||||
private class CopyToClipboardAction(private val myCoroutinesDump: List<CoroutineState>, private val myProject: Project) :
|
||||
DumbAwareAction("Copy to Clipboard", "Copy whole coroutine dump to clipboard", PlatformIcons.COPY_ICON) {
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val buf = StringBuilder()
|
||||
buf.append("Full coroutine dump").append("\n\n")
|
||||
for (state in myCoroutinesDump) {
|
||||
buf.append(state.stringStackTrace).append("\n\n")
|
||||
}
|
||||
CopyPasteManager.getInstance().setContents(StringSelection(buf.toString()))
|
||||
|
||||
group.createNotification(
|
||||
"Full coroutine dump was successfully copied to clipboard",
|
||||
MessageType.INFO
|
||||
).notify(myProject)
|
||||
}
|
||||
|
||||
private val group = NotificationGroup.toolWindowGroup("Analyze coroutine dump", ToolWindowId.RUN, false)
|
||||
}
|
||||
|
||||
private fun createToFileExporter(project: Project, states: List<CoroutineState>): ExporterToTextFile {
|
||||
return MyToFileExporter(project, states)
|
||||
}
|
||||
|
||||
private class MyToFileExporter(private val myProject: Project, private val states: List<CoroutineState>) :
|
||||
ExporterToTextFile {
|
||||
|
||||
override fun getReportText(): String {
|
||||
val sb = StringBuilder()
|
||||
for (state in states) {
|
||||
sb.append(state.stringStackTrace).append("\n\n")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun getDefaultFilePath(): String {
|
||||
val baseDir = myProject.baseDir
|
||||
return if (baseDir != null) {
|
||||
baseDir.presentableUrl + File.separator + defaultReportFileName
|
||||
} else ""
|
||||
}
|
||||
|
||||
override fun canExport(): Boolean {
|
||||
return states.isNotEmpty()
|
||||
}
|
||||
|
||||
private val defaultReportFileName = "coroutines_report.txt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.sun.jdi.*
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import org.jetbrains.kotlin.idea.debugger.isSubtype
|
||||
|
||||
/**
|
||||
* Represents state of a coroutine.
|
||||
* @see `kotlinx.coroutines.debug.CoroutineInfo`
|
||||
*/
|
||||
class CoroutineState(
|
||||
val name: String,
|
||||
val state: State,
|
||||
val thread: ThreadReference? = null,
|
||||
val stackTrace: List<StackTraceElement>,
|
||||
val frame: ObjectReference?
|
||||
) {
|
||||
val isSuspended: Boolean = state == State.SUSPENDED
|
||||
val isEmptyStackTrace: Boolean by lazy { stackTrace.isEmpty() }
|
||||
val stringStackTrace: String by lazy {
|
||||
buildString {
|
||||
appendln("\"$name\", state: $state")
|
||||
stackTrace.forEach {
|
||||
appendln("\t$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
|
||||
* @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null
|
||||
*/
|
||||
private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? {
|
||||
val type = continuation.type() as ClassType
|
||||
if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null
|
||||
val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;")
|
||||
return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference
|
||||
}
|
||||
|
||||
/**
|
||||
* Find continuation for the [stackTraceElement]
|
||||
* Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found
|
||||
* @return null if matching continuation is not found or is not BaseContinuationImpl
|
||||
*/
|
||||
fun getContinuation(stackTraceElement: StackTraceElement, context: ExecutionContext): ObjectReference? {
|
||||
var continuation = frame ?: return null
|
||||
val baseType = "kotlin.coroutines.jvm.internal.BaseContinuationImpl"
|
||||
val getTrace = (continuation.type() as ClassType).concreteMethodByName(
|
||||
"getStackTraceElement",
|
||||
"()Ljava/lang/StackTraceElement;"
|
||||
)
|
||||
val stackTraceType = context.findClass("java.lang.StackTraceElement") as ClassType
|
||||
val getClassName = stackTraceType.concreteMethodByName("getClassName", "()Ljava/lang/String;")
|
||||
val getLineNumber = stackTraceType.concreteMethodByName("getLineNumber", "()I")
|
||||
val className = {
|
||||
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
|
||||
if (trace != null)
|
||||
(context.invokeMethod(trace, getClassName, emptyList()) as StringReference).value()
|
||||
else ""
|
||||
}
|
||||
val lineNumber = {
|
||||
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
|
||||
if (trace != null)
|
||||
(context.invokeMethod(trace, getLineNumber, emptyList()) as IntegerValue).value()
|
||||
else -239 // invalid line number (but well-educated)
|
||||
}
|
||||
|
||||
while (continuation.type().isSubtype(baseType)
|
||||
&& (stackTraceElement.className != className() || stackTraceElement.lineNumber != lineNumber())
|
||||
) {
|
||||
// while continuation is BaseContinuationImpl and it's frame equals to the current
|
||||
continuation = getNextFrame(continuation, context) ?: return null
|
||||
}
|
||||
return if (continuation.type().isSubtype(baseType)) continuation else null
|
||||
}
|
||||
|
||||
enum class State {
|
||||
RUNNING,
|
||||
SUSPENDED,
|
||||
CREATED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 != null
|
||||
&& 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
|
||||
if (session != null)
|
||||
registerCoroutinesPanel(session.xDebugSession?.ui ?: return@swingInvokeLater, 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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.DebugProcess
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.sun.jdi.*
|
||||
import com.sun.tools.jdi.StringReferenceImpl
|
||||
import javaslang.control.Either
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import org.jetbrains.kotlin.psi.UserDataProperty
|
||||
|
||||
object CoroutinesDebugProbesProxy {
|
||||
private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug"
|
||||
private var DebugProcess.references by UserDataProperty(Key.create<ProcessReferences>("COROUTINES_DEBUG_REFERENCES"))
|
||||
|
||||
@Suppress("unused")
|
||||
fun install(context: ExecutionContext) {
|
||||
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
|
||||
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
|
||||
val install = debugProbes.concreteMethodByName("install", "()V")
|
||||
context.invokeMethod(instance, install, emptyList())
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun uninstall(context: ExecutionContext) {
|
||||
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
|
||||
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
|
||||
val uninstall = debugProbes.concreteMethodByName("uninstall", "()V")
|
||||
context.invokeMethod(instance, uninstall, emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes DebugProbes from debugged process's classpath and returns states of coroutines
|
||||
* Should be invoked on debugger manager thread
|
||||
*/
|
||||
fun dumpCoroutines(context: ExecutionContext): Either<Throwable, List<CoroutineState>> {
|
||||
try {
|
||||
if (context.debugProcess.references == null) {
|
||||
context.debugProcess.references = ProcessReferences(context)
|
||||
}
|
||||
val refs = context.debugProcess.references!! // already initialized if it was null
|
||||
|
||||
// get dump
|
||||
val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as ObjectReference
|
||||
context.keepReference(infoList)
|
||||
val size = (context.invokeMethod(infoList, refs.getSize, emptyList()) as IntegerValue).value()
|
||||
|
||||
return Either.right(List(size) {
|
||||
val index = context.vm.mirrorOf(it)
|
||||
val elem = context.invokeMethod(infoList, refs.getElement, listOf(index)) as ObjectReference
|
||||
val name = getName(context, elem, refs)
|
||||
val state = getState(context, elem, refs)
|
||||
val thread = getLastObservedThread(elem, refs.threadRef)
|
||||
CoroutineState(
|
||||
name,
|
||||
CoroutineState.State.valueOf(state),
|
||||
thread,
|
||||
getStackTrace(elem, refs, context),
|
||||
elem.getValue(refs.continuation) as? ObjectReference
|
||||
)
|
||||
})
|
||||
} catch (e: Throwable) {
|
||||
return Either.left(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getName(
|
||||
context: ExecutionContext, // Execution context to invoke methods
|
||||
info: ObjectReference, // CoroutineInfo instance
|
||||
refs: ProcessReferences
|
||||
): String {
|
||||
// equals to `coroutineInfo.context.get(CoroutineName).name`
|
||||
val coroutineContextInst = context.invokeMethod(info, refs.getContext, emptyList()) as ObjectReference
|
||||
val coroutineName = context.invokeMethod(
|
||||
coroutineContextInst,
|
||||
refs.getContextElement, listOf(refs.nameKey)
|
||||
) as? ObjectReference
|
||||
// If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null
|
||||
val name = if (coroutineName != null) (context.invokeMethod(
|
||||
coroutineName,
|
||||
refs.getName, emptyList()
|
||||
) as StringReferenceImpl).value() else "coroutine"
|
||||
val id = (info.getValue(refs.idField) as LongValue).value()
|
||||
return "$name#$id"
|
||||
}
|
||||
|
||||
private fun getState(
|
||||
context: ExecutionContext, // Execution context to invoke methods
|
||||
info: ObjectReference, // CoroutineInfo instance
|
||||
refs: ProcessReferences
|
||||
): String {
|
||||
// equals to stringState = coroutineInfo.state.toString()
|
||||
val state = context.invokeMethod(info, refs.getState, emptyList()) as ObjectReference
|
||||
return (context.invokeMethod(state, refs.toString, emptyList()) as StringReferenceImpl).value()
|
||||
}
|
||||
|
||||
private fun getLastObservedThread(
|
||||
info: ObjectReference, // CoroutineInfo instance
|
||||
threadRef: Field // reference to lastObservedThread
|
||||
): ThreadReference? = info.getValue(threadRef) as ThreadReference?
|
||||
|
||||
/**
|
||||
* Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference]
|
||||
*/
|
||||
private fun getStackTrace(
|
||||
info: ObjectReference,
|
||||
refs: ProcessReferences,
|
||||
context: ExecutionContext
|
||||
): List<StackTraceElement> {
|
||||
val frameList = context.invokeMethod(info, refs.lastObservedStackTrace, emptyList()) as ObjectReference
|
||||
val mergedFrameList = context.invokeMethod(
|
||||
refs.debugProbesImpl,
|
||||
refs.enhanceStackTraceWithThreadDump, listOf(info, frameList)
|
||||
) as ObjectReference
|
||||
val size = (context.invokeMethod(mergedFrameList, refs.getSize, emptyList()) as IntegerValue).value()
|
||||
|
||||
val list = ArrayList<StackTraceElement>()
|
||||
for (it in size - 1 downTo 0) {
|
||||
val frame = context.invokeMethod(
|
||||
mergedFrameList, refs.getElement,
|
||||
listOf(context.vm.virtualMachine.mirrorOf(it))
|
||||
) as ObjectReference
|
||||
val clazz = (frame.getValue(refs.className) as StringReference).value()
|
||||
|
||||
// if (clazz.contains("$DEBUG_PACKAGE.DebugProbes")) break // cut off debug intrinsic stacktrace
|
||||
list.add(
|
||||
0, // add in the beginning
|
||||
StackTraceElement(
|
||||
clazz,
|
||||
(frame.getValue(refs.methodName) as StringReference).value(),
|
||||
(frame.getValue(refs.fileName) as StringReference?)?.value(),
|
||||
(frame.getValue(refs.line) as IntegerValue).value()
|
||||
)
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm
|
||||
*/
|
||||
private class ProcessReferences(context: ExecutionContext) {
|
||||
// kotlinx.coroutines.debug.DebugProbes instance and methods
|
||||
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
|
||||
val probesImplType = context.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType
|
||||
val debugProbesImpl = with(probesImplType) { getValue(fieldByName("INSTANCE")) as ObjectReference }
|
||||
val enhanceStackTraceWithThreadDump: Method = probesImplType
|
||||
.methodsByName("enhanceStackTraceWithThreadDump").single()
|
||||
|
||||
val dumpMethod: Method = debugProbes.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;")
|
||||
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
|
||||
|
||||
// CoroutineInfo
|
||||
val info = context.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType
|
||||
val getState: Method = info.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;")
|
||||
val getContext: Method = info.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;")
|
||||
val idField: Field = info.fieldByName("sequenceNumber")
|
||||
val lastObservedStackTrace: Method = info.methodsByName("lastObservedStackTrace").single()
|
||||
val coroutineContext = context.findClass("kotlin.coroutines.CoroutineContext") as InterfaceType
|
||||
val getContextElement: Method = coroutineContext.methodsByName("get").single()
|
||||
val coroutineName = context.findClass("kotlinx.coroutines.CoroutineName") as ClassType
|
||||
val getName: Method = coroutineName.methodsByName("getName").single()
|
||||
val nameKey = coroutineName.getValue(coroutineName.fieldByName("Key")) as ObjectReference
|
||||
val toString: Method = (context.findClass("java.lang.Object") as ClassType)
|
||||
.methodsByName("toString").single()
|
||||
|
||||
val threadRef: Field = info.fieldByName("lastObservedThread")
|
||||
val continuation: Field = info.fieldByName("lastObservedFrame")
|
||||
|
||||
// Methods for list
|
||||
val listType = context.findClass("java.util.List") as InterfaceType
|
||||
val getSize: Method = listType.methodsByName("size").single()
|
||||
val getElement: Method = listType.methodsByName("get").single()
|
||||
val element = context.findClass("java.lang.StackTraceElement") as ClassType
|
||||
|
||||
// for StackTraceElement
|
||||
val methodName: Field = element.fieldByName("methodName")
|
||||
val className: Field = element.fieldByName("declaringClass")
|
||||
val fileName: Field = element.fieldByName("fileName")
|
||||
val line: Field = element.fieldByName("lineNumber")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* 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.DebuggerBundle
|
||||
import com.intellij.debugger.DebuggerInvocationUtil
|
||||
import com.intellij.debugger.DebuggerManagerEx
|
||||
import com.intellij.debugger.actions.GotoFrameSourceAction
|
||||
import com.intellij.debugger.engine.*
|
||||
import com.intellij.debugger.engine.evaluation.EvaluateException
|
||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||
import com.intellij.debugger.engine.events.DebuggerCommandImpl
|
||||
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
||||
import com.intellij.debugger.impl.DebuggerContextImpl
|
||||
import com.intellij.debugger.impl.DebuggerSession
|
||||
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
|
||||
import com.intellij.debugger.memory.utils.StackFrameItem
|
||||
import com.intellij.debugger.ui.impl.tree.TreeBuilder
|
||||
import com.intellij.debugger.ui.impl.tree.TreeBuilderNode
|
||||
import com.intellij.debugger.ui.impl.watch.*
|
||||
import com.intellij.debugger.ui.tree.StackFrameDescriptor
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.MessageType
|
||||
import com.intellij.psi.JavaPsiFacade
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.ui.DoubleClickListener
|
||||
import com.intellij.xdebugger.XDebuggerUtil
|
||||
import com.intellij.xdebugger.XSourcePosition
|
||||
import com.intellij.xdebugger.frame.XExecutionStack
|
||||
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
|
||||
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
|
||||
import com.sun.jdi.ClassType
|
||||
import javaslang.control.Either
|
||||
import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.createExecutionContext
|
||||
import java.awt.event.MouseEvent
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.swing.event.TreeModelEvent
|
||||
import javax.swing.event.TreeModelListener
|
||||
|
||||
/**
|
||||
* Tree of coroutines for [CoroutinesPanel]
|
||||
*/
|
||||
class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
|
||||
private val logger = Logger.getInstance(this::class.java)
|
||||
private var lastSuspendContextDump: Pair<WeakReference<SuspendContextImpl>, Either<Throwable, List<CoroutineState>>>? = null
|
||||
|
||||
override fun createNodeManager(project: Project): NodeManagerImpl {
|
||||
return object : NodeManagerImpl(project, this) {
|
||||
override fun getContextKey(frame: StackFrameProxyImpl?): String? {
|
||||
return "CoroutinesView"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare specific behavior instead of DebuggerTree constructor
|
||||
*/
|
||||
init {
|
||||
val model = object : TreeBuilder(this) {
|
||||
override fun buildChildren(node: TreeBuilderNode) {
|
||||
val debuggerTreeNode = node as DebuggerTreeNodeImpl
|
||||
if (debuggerTreeNode.descriptor is DefaultNodeDescriptor) {
|
||||
return
|
||||
}
|
||||
|
||||
node.add(myNodeManager.createMessageNode(MessageDescriptor.EVALUATING))
|
||||
buildNode(debuggerTreeNode)
|
||||
}
|
||||
|
||||
override fun isExpandable(builderNode: TreeBuilderNode): Boolean {
|
||||
return this@CoroutinesDebuggerTree.isExpandable(builderNode as DebuggerTreeNodeImpl)
|
||||
}
|
||||
}
|
||||
model.setRoot(nodeFactory.defaultNode)
|
||||
model.addTreeModelListener(createListener())
|
||||
|
||||
setModel(model)
|
||||
emptyText.text = "Coroutines are not available"
|
||||
}
|
||||
|
||||
/**
|
||||
* Add frames inside coroutine (node)
|
||||
*/
|
||||
private fun buildNode(node: DebuggerTreeNodeImpl) {
|
||||
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||
val debugProcess = context.debugProcess
|
||||
debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||
override fun contextAction(suspendContext: SuspendContextImpl) {
|
||||
val evalContext = debuggerContext.createEvaluationContext() ?: return
|
||||
if (node.descriptor is CoroutineDescriptorImpl || node.descriptor is CreationFramesDescriptor) {
|
||||
val children = mutableListOf<DebuggerTreeNodeImpl>()
|
||||
try {
|
||||
addChildren(children, debugProcess, node.descriptor, evalContext)
|
||||
} catch (e: EvaluateException) {
|
||||
children.clear()
|
||||
children.add(myNodeManager.createMessageNode(e.message))
|
||||
logger.debug(e)
|
||||
}
|
||||
DebuggerInvocationUtil.swingInvokeLater(project) {
|
||||
node.removeAllChildren()
|
||||
for (debuggerTreeNode in children) {
|
||||
node.add(debuggerTreeNode)
|
||||
}
|
||||
node.childrenChanged(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun installAction(): () -> Unit {
|
||||
val listener = object : DoubleClickListener() {
|
||||
override fun onDoubleClick(e: MouseEvent): Boolean {
|
||||
val location = getPathForLocation(e.x, e.y)
|
||||
?.lastPathComponent as? DebuggerTreeNodeImpl ?: return false
|
||||
return selectFrame(location.userObject)
|
||||
}
|
||||
}
|
||||
listener.installOn(this)
|
||||
|
||||
return { listener.uninstall(this) }
|
||||
}
|
||||
|
||||
fun selectFrame(descriptor: Any): Boolean {
|
||||
val dataContext = DataManager.getInstance().getDataContext(this@CoroutinesDebuggerTree)
|
||||
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||
when (descriptor) {
|
||||
is SuspendStackFrameDescriptor -> {
|
||||
buildSuspendStackFrameChildren(descriptor)
|
||||
return true
|
||||
}
|
||||
is AsyncStackFrameDescriptor -> {
|
||||
buildAsyncStackFrameChildren(descriptor, context.debugProcess ?: return false)
|
||||
return true
|
||||
}
|
||||
is EmptyStackFrameDescriptor -> {
|
||||
buildEmptyStackFrameChildren(descriptor)
|
||||
return true
|
||||
}
|
||||
is StackFrameDescriptor -> {
|
||||
GotoFrameSourceAction.doAction(dataContext)
|
||||
return true
|
||||
}
|
||||
else -> return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSuspendStackFrameChildren(descriptor: SuspendStackFrameDescriptor) {
|
||||
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||
val pos = getPosition(descriptor.frame) ?: return
|
||||
context.debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||
override fun contextAction() {
|
||||
val (stack, stackFrame) = createSyntheticStackFrame(descriptor, pos) ?: return
|
||||
val action: () -> Unit = { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(stack, stackFrame) }
|
||||
ApplicationManager.getApplication()
|
||||
.invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun buildAsyncStackFrameChildren(descriptor: AsyncStackFrameDescriptor, process: DebugProcessImpl) {
|
||||
process.managerThread?.schedule(object : DebuggerCommandImpl() {
|
||||
override fun action() {
|
||||
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||
val proxy = ThreadReferenceProxyImpl(
|
||||
process.virtualMachineProxy,
|
||||
descriptor.state.thread // is not null because it's a running coroutine
|
||||
)
|
||||
val executionStack = JavaExecutionStack(proxy, process, false)
|
||||
executionStack.initTopFrame()
|
||||
val frame = descriptor.frame.createFrame(process)
|
||||
DebuggerUIUtil.invokeLater {
|
||||
context.debuggerSession?.xDebugSession?.setCurrentStackFrame(
|
||||
executionStack,
|
||||
frame
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun buildEmptyStackFrameChildren(descriptor: EmptyStackFrameDescriptor) {
|
||||
val position = getPosition(descriptor.frame) ?: return
|
||||
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||
val suspendContext = context.suspendContext ?: return
|
||||
val proxy = suspendContext.thread ?: return
|
||||
context.debugProcess?.managerThread?.schedule(object : DebuggerCommandImpl() {
|
||||
override fun action() {
|
||||
val executionStack =
|
||||
JavaExecutionStack(proxy, context.debugProcess!!, false)
|
||||
executionStack.initTopFrame()
|
||||
val frame = SyntheticStackFrame(descriptor, emptyList(), position)
|
||||
val action: () -> Unit =
|
||||
{ context.debuggerSession?.xDebugSession?.setCurrentStackFrame(executionStack, frame) }
|
||||
ApplicationManager.getApplication()
|
||||
.invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun getPosition(frame: StackTraceElement): XSourcePosition? {
|
||||
|
||||
val psiFacade = JavaPsiFacade.getInstance(project)
|
||||
val psiClass = psiFacade.findClass(
|
||||
frame.className.substringBefore("$"), // find outer class, for which psi exists TODO
|
||||
GlobalSearchScope.everythingScope(project)
|
||||
)
|
||||
val classFile = psiClass?.containingFile?.virtualFile
|
||||
return XDebuggerUtil.getInstance().createPosition(classFile, frame.lineNumber)
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be invoked on manager thread
|
||||
*/
|
||||
private fun createSyntheticStackFrame(
|
||||
descriptor: SuspendStackFrameDescriptor,
|
||||
pos: XSourcePosition
|
||||
): Pair<XExecutionStack, SyntheticStackFrame>? {
|
||||
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||
val proxy = context.suspendContext?.thread ?: return null
|
||||
val executionStack =
|
||||
JavaExecutionStack(proxy, context.debugProcess!!, false)
|
||||
executionStack.initTopFrame()
|
||||
val execContext = context.createExecutionContext() ?: return null
|
||||
val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl
|
||||
val aMethod = (continuation.type() as ClassType).concreteMethodByName(
|
||||
"getStackTraceElement",
|
||||
"()Ljava/lang/StackTraceElement;"
|
||||
)
|
||||
val debugMetadataKtType = execContext
|
||||
.findClass("kotlin.coroutines.jvm.internal.DebugMetadataKt") as ClassType
|
||||
val vars = with(KotlinCoroutinesAsyncStackTraceProvider()) {
|
||||
KotlinCoroutinesAsyncStackTraceProvider.AsyncStackTraceContext(
|
||||
execContext,
|
||||
aMethod,
|
||||
debugMetadataKtType
|
||||
).getSpilledVariables(continuation)
|
||||
} ?: return null
|
||||
return executionStack to SyntheticStackFrame(descriptor, vars, pos)
|
||||
}
|
||||
|
||||
private fun addChildren(
|
||||
children: MutableList<DebuggerTreeNodeImpl>,
|
||||
debugProcess: DebugProcessImpl,
|
||||
descriptor: NodeDescriptorImpl,
|
||||
evalContext: EvaluationContextImpl
|
||||
) {
|
||||
if (descriptor !is CoroutineDescriptorImpl) {
|
||||
if (descriptor is CreationFramesDescriptor) {
|
||||
val threadProxy = debuggerContext.suspendContext?.thread ?: return
|
||||
val proxy = threadProxy.forceFrames().first()
|
||||
descriptor.frames.forEach {
|
||||
children.add(myNodeManager.createNode(EmptyStackFrameDescriptor(it, proxy), evalContext))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
when (descriptor.state.state) {
|
||||
CoroutineState.State.RUNNING -> {
|
||||
if (descriptor.state.thread == null) {
|
||||
children.add(myNodeManager.createMessageNode("Frames are not available"))
|
||||
return
|
||||
}
|
||||
val proxy = ThreadReferenceProxyImpl(
|
||||
debugProcess.virtualMachineProxy,
|
||||
descriptor.state.thread
|
||||
)
|
||||
val frames = proxy.forceFrames()
|
||||
var i = frames.lastIndex
|
||||
while (i > 0 && frames[i].location().method().name() != "resumeWith") i--
|
||||
// if i is less than 0, wait, what?
|
||||
for (frame in 0..--i) {
|
||||
children.add(createFrameDescriptor(descriptor, evalContext, frames[frame]))
|
||||
}
|
||||
if (i > 0) { // add async stack trace if there are frames after invokeSuspend
|
||||
val async = KotlinCoroutinesAsyncStackTraceProvider().getAsyncStackTrace(
|
||||
JavaStackFrame(StackFrameDescriptorImpl(frames[i - 1], MethodsTracker()), true),
|
||||
evalContext.suspendContext
|
||||
)
|
||||
async?.forEach { children.add(createAsyncFrameDescriptor(descriptor, evalContext, it, frames[0])) }
|
||||
}
|
||||
for (frame in i + 2..frames.lastIndex) {
|
||||
children.add(createFrameDescriptor(descriptor, evalContext, frames[frame]))
|
||||
}
|
||||
}
|
||||
CoroutineState.State.SUSPENDED -> {
|
||||
val threadProxy = debuggerContext.suspendContext?.thread ?: return
|
||||
val proxy = threadProxy.forceFrames().first()
|
||||
// the thread is paused on breakpoint - it has at least one frame
|
||||
for (it in descriptor.state.stackTrace) {
|
||||
if (it.className.startsWith("\b\b\b")) break
|
||||
children.add(createCoroutineFrameDescriptor(descriptor, evalContext, it, proxy))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
val trace = descriptor.state.stackTrace
|
||||
val index = trace.indexOfFirst { it.className.startsWith("\b\b\b") }
|
||||
children.add(myNodeManager.createNode(CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext))
|
||||
}
|
||||
|
||||
|
||||
private fun createFrameDescriptor(
|
||||
descriptor: NodeDescriptorImpl,
|
||||
evalContext: EvaluationContextImpl,
|
||||
frame: StackFrameProxyImpl
|
||||
): DebuggerTreeNodeImpl {
|
||||
return myNodeManager.createNode(
|
||||
myNodeManager.getStackFrameDescriptor(descriptor, frame),
|
||||
evalContext
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCoroutineFrameDescriptor(
|
||||
descriptor: CoroutineDescriptorImpl,
|
||||
evalContext: EvaluationContextImpl,
|
||||
frame: StackTraceElement,
|
||||
proxy: StackFrameProxyImpl,
|
||||
parent: NodeDescriptorImpl? = null
|
||||
): DebuggerTreeNodeImpl {
|
||||
return myNodeManager.createNode(
|
||||
myNodeManager.getDescriptor(
|
||||
parent,
|
||||
CoroutineStackFrameData(descriptor.state, frame, proxy)
|
||||
), evalContext
|
||||
)
|
||||
}
|
||||
|
||||
private fun createAsyncFrameDescriptor(
|
||||
descriptor: CoroutineDescriptorImpl,
|
||||
evalContext: EvaluationContextImpl,
|
||||
frame: StackFrameItem,
|
||||
proxy: StackFrameProxyImpl
|
||||
): DebuggerTreeNodeImpl {
|
||||
return myNodeManager.createNode(
|
||||
myNodeManager.getDescriptor(
|
||||
descriptor,
|
||||
CoroutineStackFrameData(descriptor.state, frame, proxy)
|
||||
), evalContext
|
||||
)
|
||||
}
|
||||
|
||||
private fun createListener() = object : TreeModelListener {
|
||||
override fun treeNodesChanged(event: TreeModelEvent) {
|
||||
hideTooltip()
|
||||
}
|
||||
|
||||
override fun treeNodesInserted(event: TreeModelEvent) {
|
||||
hideTooltip()
|
||||
}
|
||||
|
||||
override fun treeNodesRemoved(event: TreeModelEvent) {
|
||||
hideTooltip()
|
||||
}
|
||||
|
||||
override fun treeStructureChanged(event: TreeModelEvent) {
|
||||
hideTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean {
|
||||
val descriptor = node.descriptor
|
||||
return if (descriptor is StackFrameDescriptor) return false else descriptor.isExpandable
|
||||
}
|
||||
|
||||
override fun build(context: DebuggerContextImpl) {
|
||||
val session = context.debuggerSession
|
||||
val command = RefreshCoroutinesTreeCommand(session, context.suspendContext)
|
||||
|
||||
val state = if (session != null) session.state else DebuggerSession.State.DISPOSED
|
||||
if (ApplicationManager.getApplication().isUnitTestMode
|
||||
|| state == DebuggerSession.State.PAUSED
|
||||
) {
|
||||
showMessage(MessageDescriptor.EVALUATING)
|
||||
context.debugProcess!!.managerThread.schedule(command)
|
||||
} else {
|
||||
showMessage(if (session != null) session.stateDescription else DebuggerBundle.message("status.debug.stopped"))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RefreshCoroutinesTreeCommand(private val mySession: DebuggerSession?, context: SuspendContextImpl?) :
|
||||
SuspendContextCommandImpl(context) {
|
||||
|
||||
override fun contextAction() {
|
||||
val root = nodeFactory.defaultNode
|
||||
mySession ?: return
|
||||
val suspendContext = suspendContext
|
||||
if (suspendContext == null || suspendContext.isResumed) {
|
||||
setRoot(root.apply { add(myNodeManager.createMessageNode("Application is resumed")) })
|
||||
return
|
||||
}
|
||||
val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
|
||||
val executionContext = ExecutionContext(evaluationContext, suspendContext.frameProxy ?: return)
|
||||
val cache = lastSuspendContextDump
|
||||
val states = if (cache != null && cache.first.get() === suspendContext) {
|
||||
cache.second
|
||||
} else CoroutinesDebugProbesProxy.dumpCoroutines(executionContext).apply {
|
||||
lastSuspendContextDump = WeakReference(suspendContext) to this
|
||||
}
|
||||
// if suspend context hasn't changed - use last dump, else compute new
|
||||
if (states.isLeft) {
|
||||
logger.warn(states.left)
|
||||
setRoot(root.apply {
|
||||
clear()
|
||||
add(nodeFactory.createMessageNode(MessageDescriptor("Dump failed")))
|
||||
})
|
||||
XDebuggerManagerImpl.NOTIFICATION_GROUP
|
||||
.createNotification(
|
||||
"Coroutine dump failed. See log",
|
||||
MessageType.ERROR
|
||||
).notify(project)
|
||||
return
|
||||
}
|
||||
for (state in states.get()) {
|
||||
root.add(
|
||||
nodeFactory.createNode(
|
||||
nodeFactory.getDescriptor(null, CoroutineData(state)), evaluationContext
|
||||
)
|
||||
)
|
||||
}
|
||||
setRoot(root)
|
||||
}
|
||||
|
||||
private fun setRoot(root: DebuggerTreeNodeImpl) {
|
||||
DebuggerInvocationUtil.swingInvokeLater(project) {
|
||||
mutableModel.setRoot(root)
|
||||
treeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||
|
||||
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
||||
import com.intellij.debugger.impl.DebuggerContextImpl
|
||||
import com.intellij.debugger.impl.DebuggerContextListener
|
||||
import com.intellij.debugger.impl.DebuggerSession
|
||||
import com.intellij.debugger.impl.DebuggerStateManager
|
||||
import com.intellij.debugger.ui.impl.DebuggerTreePanel
|
||||
import com.intellij.debugger.ui.impl.watch.DebuggerTree
|
||||
import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.ActionPopupMenu
|
||||
import com.intellij.openapi.actionSystem.EmptyActionGroup
|
||||
import com.intellij.openapi.actionSystem.PlatformDataKeys
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.ScrollPaneFactory
|
||||
import com.intellij.util.Alarm
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Actually added into ui in [CoroutinesDebugConfigurationExtension.registerCoroutinesPanel]
|
||||
* Some methods are copied from [com.intellij.debugger.ui.impl.ThreadsPanel]
|
||||
*/
|
||||
class CoroutinesPanel(project: Project, stateManager: DebuggerStateManager) : DebuggerTreePanel(project, stateManager) {
|
||||
private val myUpdateLabelsAlarm = Alarm(Alarm.ThreadToUse.SWING_THREAD)
|
||||
|
||||
init {
|
||||
val disposable = getCoroutinesTree().installAction()
|
||||
registerDisposable(disposable)
|
||||
getCoroutinesTree().addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyCode == KeyEvent.VK_ENTER && getCoroutinesTree().selectionCount == 1) {
|
||||
val selected = getCoroutinesTree().selectionModel.selectionPath.lastPathComponent
|
||||
if (selected is DebuggerTreeNodeImpl) getCoroutinesTree().selectFrame(selected.userObject)
|
||||
}
|
||||
}
|
||||
})
|
||||
add(ScrollPaneFactory.createScrollPane(getCoroutinesTree()), BorderLayout.CENTER)
|
||||
stateManager.addListener(object : DebuggerContextListener {
|
||||
override fun changeEvent(newContext: DebuggerContextImpl, event: DebuggerSession.Event) {
|
||||
if (DebuggerSession.Event.ATTACHED == event || DebuggerSession.Event.RESUME == event) {
|
||||
startLabelsUpdate()
|
||||
} else if (DebuggerSession.Event.PAUSE == event
|
||||
|| DebuggerSession.Event.DETACHED == event
|
||||
|| DebuggerSession.Event.DISPOSE == event
|
||||
) {
|
||||
myUpdateLabelsAlarm.cancelAllRequests()
|
||||
}
|
||||
if (DebuggerSession.Event.DETACHED == event || DebuggerSession.Event.DISPOSE == event) {
|
||||
stateManager.removeListener(this)
|
||||
}
|
||||
}
|
||||
})
|
||||
startLabelsUpdate()
|
||||
}
|
||||
|
||||
private fun startLabelsUpdate() {
|
||||
if (myUpdateLabelsAlarm.isDisposed) return
|
||||
myUpdateLabelsAlarm.cancelAllRequests()
|
||||
myUpdateLabelsAlarm.addRequest(object : Runnable {
|
||||
override fun run() {
|
||||
var updateScheduled = false
|
||||
try {
|
||||
if (isUpdateEnabled) {
|
||||
val tree = getCoroutinesTree()
|
||||
val root = tree.model.root as DebuggerTreeNodeImpl
|
||||
val process = context.debugProcess
|
||||
if (process != null) {
|
||||
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||
override fun contextAction() {
|
||||
try {
|
||||
updateNodeLabels(root)
|
||||
} finally {
|
||||
reschedule()
|
||||
}
|
||||
}
|
||||
|
||||
override fun commandCancelled() {
|
||||
reschedule()
|
||||
}
|
||||
})
|
||||
updateScheduled = true
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!updateScheduled) {
|
||||
reschedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reschedule() {
|
||||
val session = context.debuggerSession
|
||||
if (session != null && session.isAttached && !session.isPaused && !myUpdateLabelsAlarm.isDisposed) {
|
||||
myUpdateLabelsAlarm.addRequest(
|
||||
this,
|
||||
LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}, LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
Disposer.dispose(myUpdateLabelsAlarm)
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
private fun updateNodeLabels(from: DebuggerTreeNodeImpl) {
|
||||
val children = from.children()
|
||||
try {
|
||||
while (children.hasMoreElements()) {
|
||||
val child = children.nextElement() as DebuggerTreeNodeImpl
|
||||
child.descriptor.updateRepresentation(
|
||||
null
|
||||
) { child.labelChanged() }
|
||||
updateNodeLabels(child)
|
||||
}
|
||||
} catch (ignored: NoSuchElementException) { // children have changed - just skip
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun createTreeView(): DebuggerTree {
|
||||
return CoroutinesDebuggerTree(project)
|
||||
}
|
||||
|
||||
override fun createPopupMenu(): ActionPopupMenu {
|
||||
val group = EmptyActionGroup()
|
||||
return ActionManager.getInstance().createActionPopupMenu("Debugger.CoroutinesPanelPopup", group)
|
||||
}
|
||||
|
||||
override fun getData(dataId: String): Any? {
|
||||
return if (PlatformDataKeys.HELP_ID.`is`(dataId)) {
|
||||
HELP_ID
|
||||
} else super.getData(dataId)
|
||||
}
|
||||
|
||||
fun getCoroutinesTree(): CoroutinesDebuggerTree = tree as CoroutinesDebuggerTree
|
||||
|
||||
companion object {
|
||||
@NonNls
|
||||
private val HELP_ID = "debugging.debugCoroutines"
|
||||
private const val LABELS_UPDATE_DELAY_MS = 200
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.JavaStackFrame
|
||||
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
|
||||
import com.intellij.xdebugger.XSourcePosition
|
||||
import com.intellij.xdebugger.frame.XCompositeNode
|
||||
import com.intellij.xdebugger.frame.XNamedValue
|
||||
import com.intellij.xdebugger.frame.XValueChildrenList
|
||||
|
||||
/**
|
||||
* Puts the frameProxy into JavaStackFrame just to instantiate. SyntheticStackFrame provides it's own data for variables view.
|
||||
*/
|
||||
class SyntheticStackFrame(
|
||||
descriptor: StackFrameDescriptorImpl,
|
||||
private val vars: List<XNamedValue>,
|
||||
private val position: XSourcePosition
|
||||
) :
|
||||
JavaStackFrame(descriptor, true) {
|
||||
|
||||
override fun computeChildren(node: XCompositeNode) {
|
||||
val list = XValueChildrenList()
|
||||
vars.forEach { list.add(it) }
|
||||
node.addChildren(list, true)
|
||||
}
|
||||
|
||||
override fun getSourcePosition(): XSourcePosition? {
|
||||
return position
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
val frame = other as? JavaStackFrame ?: return false
|
||||
|
||||
return descriptor.frameProxy == frame.descriptor.frameProxy
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return descriptor.frameProxy.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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.DebuggerManagerEx
|
||||
import com.intellij.debugger.engine.SuspendContextImpl
|
||||
import com.intellij.debugger.engine.evaluation.EvaluateException
|
||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||
import com.intellij.debugger.impl.DebuggerUtilsEx
|
||||
import com.intellij.debugger.impl.descriptors.data.DescriptorData
|
||||
import com.intellij.debugger.impl.descriptors.data.DisplayKey
|
||||
import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey
|
||||
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||
import com.intellij.debugger.memory.utils.StackFrameItem
|
||||
import com.intellij.debugger.ui.impl.watch.MethodsTracker
|
||||
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
|
||||
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
|
||||
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
import com.sun.jdi.ClassType
|
||||
import com.sun.jdi.ObjectReference
|
||||
import org.jetbrains.kotlin.idea.IconExtensionChooser
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* Describes coroutine itself in the tree (name: STATE), has children if stacktrace is not empty (state = CREATED)
|
||||
*/
|
||||
class CoroutineData(private val state: CoroutineState) : DescriptorData<CoroutineDescriptorImpl>() {
|
||||
|
||||
override fun createDescriptorImpl(project: Project): CoroutineDescriptorImpl {
|
||||
return CoroutineDescriptorImpl(state)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other !is CoroutineData) {
|
||||
false
|
||||
} else state.name == other.state.name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return state.name.hashCode()
|
||||
}
|
||||
|
||||
override fun getDisplayKey(): DisplayKey<CoroutineDescriptorImpl> {
|
||||
return SimpleDisplayKey(state.name)
|
||||
}
|
||||
}
|
||||
|
||||
class CoroutineDescriptorImpl(val state: CoroutineState) : NodeDescriptorImpl() {
|
||||
var suspendContext: SuspendContextImpl? = null
|
||||
val icon: Icon
|
||||
get() = when {
|
||||
state.isSuspended -> AllIcons.Debugger.ThreadSuspended
|
||||
state.state == CoroutineState.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
|
||||
else -> AllIcons.Debugger.ThreadRunning
|
||||
}
|
||||
|
||||
override fun getName(): String? {
|
||||
return state.name
|
||||
}
|
||||
|
||||
@Throws(EvaluateException::class)
|
||||
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String {
|
||||
val name = if (state.thread != null) state.thread.name().substringBefore(" @${state.name}") else ""
|
||||
val threadState = if (state.thread != null) DebuggerUtilsEx.getThreadStatusText(state.thread.status()) else ""
|
||||
return "${state.name}: ${state.state}${if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""}"
|
||||
}
|
||||
|
||||
override fun isExpandable(): Boolean {
|
||||
return state.state != CoroutineState.State.CREATED
|
||||
}
|
||||
|
||||
override fun setContext(context: EvaluationContextImpl?) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class CoroutineStackFrameData private constructor(val state: CoroutineState, private val proxy: StackFrameProxyImpl) :
|
||||
DescriptorData<NodeDescriptorImpl>() {
|
||||
private var frame: StackTraceElement? = null
|
||||
private var frameItem: StackFrameItem? = null
|
||||
|
||||
constructor(state: CoroutineState, frame: StackTraceElement, proxy: StackFrameProxyImpl) : this(state, proxy) {
|
||||
this.frame = frame
|
||||
}
|
||||
|
||||
constructor(state: CoroutineState, frameItem: StackFrameItem, proxy: StackFrameProxyImpl) : this(state, proxy) {
|
||||
this.frameItem = frameItem
|
||||
}
|
||||
|
||||
override fun hashCode() = frame?.hashCode() ?: frameItem.hashCode()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is CoroutineStackFrameData) {
|
||||
other.frame == frame && other.frameItem == frameItem
|
||||
} else false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [EmptyStackFrameDescriptor], [SuspendStackFrameDescriptor]
|
||||
* or [AsyncStackFrameDescriptor] according to current frame
|
||||
*/
|
||||
override fun createDescriptorImpl(project: Project): NodeDescriptorImpl {
|
||||
val frame = frame ?: return AsyncStackFrameDescriptor(
|
||||
state,
|
||||
frameItem!!,
|
||||
proxy
|
||||
)
|
||||
// check whether last fun is suspend fun
|
||||
val suspendContext =
|
||||
DebuggerManagerEx.getInstanceEx(project).context.suspendContext ?: return EmptyStackFrameDescriptor(
|
||||
frame,
|
||||
proxy
|
||||
)
|
||||
val suspendProxy = suspendContext.frameProxy ?: return EmptyStackFrameDescriptor(
|
||||
frame,
|
||||
proxy
|
||||
)
|
||||
val evalContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
|
||||
val context = ExecutionContext(evalContext, suspendProxy)
|
||||
val clazz = context.findClass(frame.className) as ClassType
|
||||
val method = clazz.methodsByName(frame.methodName).last {
|
||||
val loc = it.location().lineNumber()
|
||||
loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber
|
||||
} // pick correct method if an overloaded one is given
|
||||
return if ("Lkotlin/coroutines/Continuation;)" in method.signature() ||
|
||||
method.name() == "invokeSuspend" &&
|
||||
method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" // suspend fun or invokeSuspend
|
||||
) {
|
||||
val continuation = state.getContinuation(frame, context)
|
||||
if (continuation == null) EmptyStackFrameDescriptor(
|
||||
frame,
|
||||
proxy
|
||||
) else
|
||||
SuspendStackFrameDescriptor(
|
||||
state,
|
||||
frame,
|
||||
proxy,
|
||||
continuation
|
||||
)
|
||||
} else EmptyStackFrameDescriptor(frame, proxy)
|
||||
}
|
||||
|
||||
override fun getDisplayKey(): DisplayKey<NodeDescriptorImpl> = SimpleDisplayKey(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptor for suspend functions
|
||||
*/
|
||||
class SuspendStackFrameDescriptor(
|
||||
val state: CoroutineState,
|
||||
val frame: StackTraceElement,
|
||||
proxy: StackFrameProxyImpl,
|
||||
val continuation: ObjectReference
|
||||
) :
|
||||
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
|
||||
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||
return with(frame) {
|
||||
val pack = className.substringBeforeLast(".", "")
|
||||
"$methodName:$lineNumber, ${className.substringAfterLast(".")} " +
|
||||
if (pack.isNotEmpty()) "{$pack}" else ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun isExpandable() = false
|
||||
|
||||
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) :
|
||||
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
|
||||
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||
return with(frame) {
|
||||
val pack = path().substringBeforeLast(".", "")
|
||||
"${method()}:${line()}, ${path().substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}"
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return frame.method()
|
||||
}
|
||||
|
||||
override fun isExpandable(): Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* For the case when no data inside frame is available
|
||||
*/
|
||||
class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
|
||||
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
|
||||
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||
return with(frame) {
|
||||
val pack = className.substringBeforeLast(".", "")
|
||||
"$methodName:$lineNumber, ${className.substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}"
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName() = null
|
||||
override fun isExpandable() = false
|
||||
}
|
||||
|
||||
class CreationFramesDescriptor(val frames: List<StackTraceElement>) : NodeDescriptorImpl() {
|
||||
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||
return "Coroutine creation stack trace"
|
||||
}
|
||||
|
||||
override fun setContext(context: EvaluationContextImpl?) {}
|
||||
override fun getName() = "Coroutine creation stack trace"
|
||||
override fun isExpandable() = true
|
||||
}
|
||||
@@ -6,10 +6,12 @@
|
||||
package org.jetbrains.kotlin.idea.debugger.evaluate
|
||||
|
||||
import com.intellij.debugger.engine.DebugProcessImpl
|
||||
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
|
||||
import com.intellij.debugger.engine.SuspendContextImpl
|
||||
import com.intellij.debugger.engine.evaluation.EvaluateException
|
||||
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
|
||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||
import com.intellij.debugger.impl.DebuggerContextImpl
|
||||
import com.intellij.debugger.impl.DebuggerUtilsEx
|
||||
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||
import com.intellij.debugger.jdi.VirtualMachineProxyImpl
|
||||
@@ -98,4 +100,9 @@ class ExecutionContext(val evaluationContext: EvaluationContextImpl, val framePr
|
||||
@Suppress("DEPRECATION")
|
||||
DebuggerUtilsEx.keep(reference, evaluationContext)
|
||||
}
|
||||
}
|
||||
|
||||
fun DebuggerContextImpl.createExecutionContext(): ExecutionContext? {
|
||||
val context = createEvaluationContext()
|
||||
return ExecutionContext(context ?: return null, frameProxy ?: return null)
|
||||
}
|
||||
@@ -60,6 +60,24 @@
|
||||
<add-to-group group-id="XDebugger.Settings" relative-to-action="XDebugger.Inline" anchor="after"/>
|
||||
</action>
|
||||
|
||||
<group id="Kotlin.XDebugger.Actions">
|
||||
<action id="Kotlin.XDebugger.ToggleKotlinVariableView"
|
||||
class="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesView"
|
||||
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"
|
||||
text="Show Kotlin variables only"
|
||||
/>
|
||||
<!-- TODO(design)-->
|
||||
<action id="Kotlin.XDebugger.CoroutinesDump"
|
||||
class="org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDumpAction"
|
||||
text="Get Coroutines Dump"
|
||||
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"/>
|
||||
</group>
|
||||
|
||||
<group id="Kotlin.XDebugger.Watches.Tree.Toolbar">
|
||||
<reference ref="Kotlin.XDebugger.ToggleKotlinVariableView"/>
|
||||
<add-to-group group-id="XDebugger.Watches.Tree.Toolbar" relative-to-action="XDebugger.SwitchWatchesInVariables" anchor="after"/>
|
||||
</group>
|
||||
|
||||
<action id="InspectBreakpointApplicability" class="org.jetbrains.kotlin.idea.debugger.breakpoints.InspectBreakpointApplicabilityAction"
|
||||
text="Inspect Breakpoint Applicability" internal="true">
|
||||
<add-to-group group-id="KotlinInternalGroup"/>
|
||||
@@ -103,6 +121,7 @@
|
||||
<debugger.javaBreakpointHandlerFactory implementation="org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinFunctionBreakpointHandlerFactory"/>
|
||||
<debugger.jvmSteppingCommandProvider implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSteppingCommandProvider"/>
|
||||
<debugger.simplePropertyGetterProvider implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSimpleGetterProvider"/>
|
||||
<runConfigurationExtension implementation="org.jetbrains.kotlin.idea.debugger.coroutines.CoroutinesDebugConfigurationExtension"/>
|
||||
|
||||
<framework.type implementation="org.jetbrains.kotlin.idea.framework.JavaFrameworkType"/>
|
||||
<projectTemplatesFactory implementation="org.jetbrains.kotlin.idea.framework.KotlinTemplatesFactory" />
|
||||
@@ -181,6 +200,10 @@
|
||||
description="Enable bytecode instrumentation for Kotlin classes"
|
||||
defaultValue="false"
|
||||
restartRequired="false"/>
|
||||
<registryKey key="kotlin.debugger.coroutines"
|
||||
description="Enable debugging for coroutines in Kotlin/JVM"
|
||||
defaultValue="false"
|
||||
restartRequired="false"/>
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="org.jetbrains.uast">
|
||||
|
||||
6
idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt
vendored
Normal file
6
idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package noCoroutines
|
||||
|
||||
fun main() {
|
||||
//Breakpoint!
|
||||
val s = "nothing to see here, folks"
|
||||
}
|
||||
7
idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.out
vendored
Normal file
7
idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.out
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
LineBreakpoint created at noCoroutines.kt:5
|
||||
Run Java
|
||||
Connected to the target VM
|
||||
noCoroutines.kt:5
|
||||
Disconnected from the target VM
|
||||
|
||||
Process finished with exit code 0
|
||||
13
idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt
vendored
Normal file
13
idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package threeCoroutines
|
||||
|
||||
import kotlin.random.Random
|
||||
|
||||
suspend fun main() {
|
||||
sequence {
|
||||
yield(239)
|
||||
sequence {
|
||||
//Breakpoint!
|
||||
yield(666)
|
||||
}.toList()
|
||||
}.toList()
|
||||
}
|
||||
10
idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.out
vendored
Normal file
10
idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.out
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
LineBreakpoint created at threeCoroutines.kt:10
|
||||
Run Java
|
||||
Connected to the target VM
|
||||
threeCoroutines.kt:10
|
||||
"coroutine#1", state: RUNNING
|
||||
"coroutine#2", state: RUNNING
|
||||
"coroutine#3", state: RUNNING
|
||||
Disconnected from the target VM
|
||||
|
||||
Process finished with exit code 0
|
||||
31
idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt
vendored
Normal file
31
idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package twoDumps
|
||||
|
||||
suspend fun main() {
|
||||
foo()
|
||||
sequence<Int> {
|
||||
foo()
|
||||
yield(666)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
suspend fun foo() {
|
||||
val f = 239
|
||||
bar()
|
||||
}
|
||||
|
||||
suspend fun bar() {
|
||||
var r = 1337
|
||||
//Breakpoint!
|
||||
r += 42
|
||||
}
|
||||
|
||||
suspend fun SequenceScope<Int>.foo() {
|
||||
val k = 228
|
||||
bar()
|
||||
}
|
||||
|
||||
suspend fun SequenceScope<Int>.bar() {
|
||||
var r = 1337
|
||||
//Breakpoint!
|
||||
r += 42
|
||||
}
|
||||
12
idea/testData/debugger/tinyApp/src/coroutines/twoDumps.out
vendored
Normal file
12
idea/testData/debugger/tinyApp/src/coroutines/twoDumps.out
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
LineBreakpoint created at twoDumps.kt:19
|
||||
LineBreakpoint created at twoDumps.kt:30
|
||||
Run Java
|
||||
Connected to the target VM
|
||||
twoDumps.kt:19
|
||||
"coroutine#1", state: RUNNING
|
||||
twoDumps.kt:30
|
||||
"coroutine#1", state: RUNNING
|
||||
"coroutine#2", state: RUNNING
|
||||
Disconnected from the target VM
|
||||
|
||||
Process finished with exit code 0
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
46
idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java
generated
Normal file
46
idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java
generated
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user