Debugger: Coroutines stack frames with variables & coroutine dumps

This commit is contained in:
Aleksandr Prokopyev
2019-07-08 21:06:34 +03:00
committed by Vladimir Ilmov
parent c459b2ca6e
commit 5975251a32
21 changed files with 1923 additions and 2 deletions

View File

@@ -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.*

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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
}
}

View 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.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()
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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">

View File

@@ -0,0 +1,6 @@
package noCoroutines
fun main() {
//Breakpoint!
val s = "nothing to see here, folks"
}

View 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

View File

@@ -0,0 +1,13 @@
package threeCoroutines
import kotlin.random.Random
suspend fun main() {
sequence {
yield(239)
sequence {
//Breakpoint!
yield(666)
}.toList()
}.toList()
}

View 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

View 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
}

View 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

View File

@@ -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
}
}

View 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");
}
}