From 121b26eee9ef580816bdaed09bec17c304fe8c38 Mon Sep 17 00:00:00 2001 From: Daniel Biehl Date: Sun, 12 Jan 2025 01:02:58 +0100 Subject: [PATCH] feat(intellij): implemented robot framework debugger --- intellij-client/README.md | 10 +- intellij-client/build.gradle.kts | 5 + intellij-client/gradle/libs.versions.toml | 23 +- .../actions/RobotCreateFileAction.kt | 25 +- .../debugging/RobotCodeBreakpointHandler.kt | 18 + .../RobotCodeBreakpointProperties.kt | 14 + .../debugging/RobotCodeBreakpointType.kt | 20 ++ .../debugging/RobotCodeDebugProcess.kt | 316 ++++++++++++++++++ .../debugging/RobotCodeDebugProgramRunner.kt | 46 +++ .../debugging/RobotCodeDebugProtocolClient.kt | 206 ++++++++++++ .../debugging/RobotCodeDebuggerEvaluator.kt | 68 ++++ .../debugging/RobotCodeExecutionStack.kt | 38 +++ .../debugging/RobotCodeNamedValue.kt | 53 +++ .../debugging/RobotCodeStackFrame.kt | 75 +++++ .../debugging/RobotCodeSuspendContext.kt | 26 ++ .../debugging/RobotCodeValueGroup.kt | 27 ++ .../RobotCodeXDebuggerEditorsProvider.kt | 25 ++ .../execution/RobotCodeProgramRunner.kt | 9 +- .../execution/RobotCodeRunConfiguration.kt | 18 +- .../RobotCodeRunConfigurationEditor.kt | 4 +- .../RobotCodeRunConfigurationProducer.kt | 48 +-- .../RobotCodeRunLineMarkerContributor.kt | 17 +- .../execution/RobotCodeRunProfileState.kt | 217 +++++++++++- ...RobotOutputToGeneralTestEventsConverter.kt | 149 +++++++++ .../execution/RobotRunnerConsoleProperties.kt | 44 +++ .../execution/RobotSMTestLocator.kt | 39 +++ .../lsp/RobotCodeLanguageServerFactory.kt | 4 +- .../lsp/RobotCodeLanguageServerManager.kt | 2 +- .../robotcode4ij/psi/ElementTypes.kt | 12 +- .../robotcode/robotcode4ij/psi/Elements.kt | 2 - .../settings/RobotCodeColorSettingsPage.kt | 2 +- .../testing/RobotCodeTestActionProvider.kt | 35 ++ .../testing/RobotCodeTestManager.kt | 82 ++++- .../testing/RobotCodeTestStatusListener.kt | 12 + .../robotcode/robotcode4ij/utils/NetUtils.kt | 27 ++ .../src/main/resources/META-INF/plugin.xml | 26 +- .../src/robotcode/debugger/dap_types.py | 3 + .../src/robotcode/debugger/debugger.py | 72 ++-- .../src/robotcode/debugger/id_manager.py | 64 ++++ .../src/robotcode/debugger/listeners.py | 22 +- .../debugger/src/robotcode/debugger/server.py | 2 +- .../robotcode/runner/cli/discover/discover.py | 4 +- vscode-client/extension/debugmanager.ts | 2 +- .../extension/testcontrollermanager.ts | 1 - 44 files changed, 1780 insertions(+), 134 deletions(-) create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointHandler.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointProperties.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointType.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProcess.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProgramRunner.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProtocolClient.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebuggerEvaluator.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeExecutionStack.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeNamedValue.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeStackFrame.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeSuspendContext.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeValueGroup.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeXDebuggerEditorsProvider.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotOutputToGeneralTestEventsConverter.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotRunnerConsoleProperties.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotSMTestLocator.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestActionProvider.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestStatusListener.kt create mode 100644 intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/utils/NetUtils.kt create mode 100644 packages/debugger/src/robotcode/debugger/id_manager.py diff --git a/intellij-client/README.md b/intellij-client/README.md index 6fef2720d..7403c809c 100644 --- a/intellij-client/README.md +++ b/intellij-client/README.md @@ -6,6 +6,10 @@ +**RobotCode** is a PyCharm/IntelliJ Plugin that enhances your workflow with [Robot Framework](https://robotframework.org/). +It provides a rich set of features to help you write, run, and debug your Robot Framework tests directly within your IDE. + + ⚠️ **Important Notice** ⚠️ This plugin is currently under active development and is not yet ready for production use. Please note that it may contain bugs or lack certain features. @@ -14,9 +18,6 @@ We invite you to join the Robot Framework and RobotCode community by reporting i Your feedback is greatly appreciated! 🙂 -**RobotCode** is a PyCharm/IntelliJ Plugin that enhances your workflow with [Robot Framework](https://robotframework.org/). -It provides a rich set of features to help you write, run, and debug your Robot Framework tests directly within your IDE. - ## Why RobotCode? **Built on Robot Framework Core** @@ -28,6 +29,7 @@ RobotCode is built on the Language Server Protocol (LSP), a modern standard for **Powerful Command Line Tools** RobotCode extends the Robot Framework CLI with enhanced tools for test execution, analysis, and debugging. It supports [`robot.toml`](https://robotcode.io/03_reference/) configurations, integrates a Debug Adapter Protocol (DAP) compatible debugger, and provides an interactive REPL environment for experimenting with Robot Framework commands. Modular and flexible, these tools streamline your workflow for both development and production. + ## Key Features - **Smart Code Editing**: Auto-completion, syntax highlighting, and seamless navigation. @@ -47,6 +49,7 @@ RobotCode extends the Robot Framework CLI with enhanced tools for test execution - Robot Framework 4.1 or newer - PyCharm 2024.3.1 or newer + ## Getting Started 1. Install the [RobotCode Plugin](https://plugins.jetbrains.com/plugin/26216) from the JETBRAINS Marketplace. @@ -58,6 +61,7 @@ For a more detailed guide, check out the [Let's get started](https://robotcode.i + ## Installation - Using the IDE built-in plugin system: diff --git a/intellij-client/build.gradle.kts b/intellij-client/build.gradle.kts index 5a40835b4..06da4c314 100644 --- a/intellij-client/build.gradle.kts +++ b/intellij-client/build.gradle.kts @@ -41,6 +41,10 @@ repositories { // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog dependencies { compileOnly(libs.kotlinxSerialization) + + // implementation(libs.lsp4j) + implementation(libs.lsp4jdebug) + testImplementation(kotlin("test")) testImplementation(libs.junit) @@ -119,6 +123,7 @@ intellijPlatform { ides { recommended() } + } } diff --git a/intellij-client/gradle/libs.versions.toml b/intellij-client/gradle/libs.versions.toml index 4f09b761b..a0d9996a2 100644 --- a/intellij-client/gradle/libs.versions.toml +++ b/intellij-client/gradle/libs.versions.toml @@ -1,14 +1,25 @@ [versions] # libraries -annotations = "24.1.0" -kotlinxSerialization = "1.7.1" -junit = "4.13.2" +annotations = "26.0.1" +kotlinxSerialization = "1.8.0" +junit = "5.11.4" +lsp4j = "0.21.1" # plugins changelog = "2.2.1" intelliJPlatForm = "2.2.1" -kotlin = "2.0.10" -kover = "0.8.3" +kotlin = "2.1.0" +kover = "0.9.1" + +[libraries.lsp4j] +group = "org.eclipse.lsp4j" +name = "org.eclipse.lsp4j" +version.ref = "lsp4j" + +[libraries.lsp4jdebug] +group = "org.eclipse.lsp4j" +name = "org.eclipse.lsp4j.debug" +version.ref = "lsp4j" [libraries.annotations] group = "org.jetbrains" @@ -44,3 +55,5 @@ version.ref = "kover" [plugins.kotlinSerialization] id = "org.jetbrains.kotlin.plugin.serialization" version.ref = "kotlin" + + diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCreateFileAction.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCreateFileAction.kt index 44cb95cef..e82823fc7 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCreateFileAction.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/actions/RobotCreateFileAction.kt @@ -11,24 +11,18 @@ import dev.robotcode.robotcode4ij.RobotResourceFileType import dev.robotcode.robotcode4ij.RobotSuiteFileType class RobotCreateFileAction : CreateFileFromTemplateAction( - "Robot Framework File", "Robot Framework file", - RobotIcons - .Suite -), - DumbAware { + "Robot Framework File", "Robot Framework file", RobotIcons.Suite +), DumbAware { override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) { builder.setTitle("New Robot Framework File") - FileTemplateManager.getInstance(project) - .allTemplates - .forEach { - if (it.extension == RobotSuiteFileType.defaultExtension) { - builder.addKind(it.name, RobotIcons.Suite, it.name) - } else if (it.extension == RobotResourceFileType.defaultExtension) { - builder.addKind(it.name, RobotIcons.Resource, it.name) - } + FileTemplateManager.getInstance(project).allTemplates.forEach { + if (it.extension == RobotSuiteFileType.defaultExtension) { + builder.addKind(it.name, RobotIcons.Suite, it.name) + } else if (it.extension == RobotResourceFileType.defaultExtension) { + builder.addKind(it.name, RobotIcons.Resource, it.name) } - builder - .addKind("Suite file", RobotIcons.Suite, "Robot Suite File") + } + builder.addKind("Suite file", RobotIcons.Suite, "Robot Suite File") .addKind("Resource file", RobotIcons.Resource, "Robot Resource File") } @@ -36,5 +30,4 @@ class RobotCreateFileAction : CreateFileFromTemplateAction( override fun getActionName(directory: PsiDirectory?, newName: String, templateName: String?): String { return "Create Robot Framework File" } - } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointHandler.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointHandler.kt new file mode 100644 index 000000000..9eeb14478 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointHandler.kt @@ -0,0 +1,18 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.xdebugger.breakpoints.XBreakpointHandler +import com.intellij.xdebugger.breakpoints.XLineBreakpoint + +class RobotCodeBreakpointHandler(val process: RobotCodeDebugProcess) : + XBreakpointHandler>(RobotCodeBreakpointType::class.java) { + override fun registerBreakpoint(breakpoint: XLineBreakpoint) { + process.registerBreakpoint(breakpoint) + } + + override fun unregisterBreakpoint( + breakpoint: XLineBreakpoint, + temporary: Boolean + ) { + process.unregisterBreakpoint(breakpoint) + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointProperties.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointProperties.kt new file mode 100644 index 000000000..86ab25a10 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointProperties.kt @@ -0,0 +1,14 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.xdebugger.breakpoints.XBreakpointProperties + +class RobotCodeBreakpointProperties : XBreakpointProperties() { + + override fun getState(): RobotCodeBreakpointProperties { + return this + } + + override fun loadState(state: RobotCodeBreakpointProperties) { + TODO("Not yet implemented") + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointType.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointType.kt new file mode 100644 index 000000000..342709fff --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeBreakpointType.kt @@ -0,0 +1,20 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.xdebugger.breakpoints.XLineBreakpointType + +class RobotCodeBreakpointType : XLineBreakpointType(ID, NAME) { + companion object { + private const val ID = "robotcode-line" + private const val NAME = "robotcode-line-breakpoint" + } + + override fun createBreakpointProperties(file: VirtualFile, line: Int): RobotCodeBreakpointProperties? { + return RobotCodeBreakpointProperties() + } + + override fun canPutAt(file: VirtualFile, line: Int, project: Project): Boolean { + return true + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProcess.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProcess.kt new file mode 100644 index 000000000..bb38fd528 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProcess.kt @@ -0,0 +1,316 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.execution.ExecutionResult +import com.intellij.execution.process.ProcessHandler +import com.intellij.execution.ui.ExecutionConsole +import com.intellij.openapi.ui.MessageType +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.breakpoints.XBreakpointHandler +import com.intellij.xdebugger.breakpoints.XLineBreakpoint +import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider +import com.intellij.xdebugger.frame.XSuspendContext +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.threading.coroutines.adviseSuspend +import dev.robotcode.robotcode4ij.execution.RobotCodeRunProfileState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.eclipse.lsp4j.debug.ContinueArguments +import org.eclipse.lsp4j.debug.NextArguments +import org.eclipse.lsp4j.debug.OutputEventArgumentsCategory +import org.eclipse.lsp4j.debug.SetBreakpointsArguments +import org.eclipse.lsp4j.debug.Source +import org.eclipse.lsp4j.debug.SourceBreakpoint +import org.eclipse.lsp4j.debug.StackTraceArguments +import org.eclipse.lsp4j.debug.StepInArguments +import org.eclipse.lsp4j.debug.StepOutArguments +import org.eclipse.lsp4j.debug.StoppedEventArguments +import org.eclipse.lsp4j.debug.TerminateArguments +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer + +class RobotCodeDebugProcess( + private val session: XDebugSession, + private val executionResult: ExecutionResult, + val state: RobotCodeRunProfileState +) : XDebugProcess(session) { + + private val debugClient: RobotCodeDebugProtocolClient + get() { + return state.debugClient + } + + private val debugServer: IDebugProtocolServer + get() { + return state.debugServer + } + + init { + state.afterInitialize.adviseSuspend(Lifetime.Eternal, Dispatchers.IO) { + runBlocking { sendBreakpointRequest() } + } + debugClient.onStopped.adviseSuspend(Lifetime.Eternal, Dispatchers.IO) { args -> + handleOnStopped(args) + } + debugClient.onOutput.adviseSuspend(Lifetime.Eternal, Dispatchers.IO) { args -> + + session.reportMessage( + args.output, when (args.category) { + OutputEventArgumentsCategory.STDOUT, OutputEventArgumentsCategory.CONSOLE -> MessageType.INFO + OutputEventArgumentsCategory.STDERR -> MessageType.ERROR + else -> MessageType.WARNING + } + ) + } + + // debugClient.onTerminated.adviseSuspend(Lifetime.Eternal, Dispatchers.IO) { + // session.stop() + // } + } + + private suspend fun createRobotCodeSuspendContext(threadId: Int): RobotCodeSuspendContext { + return RobotCodeSuspendContext( + debugServer.stackTrace(StackTraceArguments().apply { this.threadId = threadId }).await(), + threadId, + debugServer, + session + ) + } + + private suspend fun handleOnStopped(args: StoppedEventArguments) { + when (args.reason) { + "breakpoint" -> { + val bp = breakpoints.firstOrNull { it.id != null && it.id in args.hitBreakpointIds } + + if (bp is LineBreakpointInfo) { + if (!session.breakpointReached( + bp.breakpoint, null, createRobotCodeSuspendContext( + args + .threadId + ) + ) + ) { + debugServer.continue_(ContinueArguments().apply { + threadId = args.threadId + }).await() + } + } else { + session.positionReached(createRobotCodeSuspendContext(args.threadId)) + } + } + + "exception" -> { + // TODO session.exceptionCaught() + } + + else -> { + session.positionReached(createRobotCodeSuspendContext(args.threadId)) + } + } + removeCurrentOneTimeBreakpoint() + } + + private open class BreakPointInfo(val line: Int, var file: VirtualFile, var id: Int? = null) + private class LineBreakpointInfo(val breakpoint: XLineBreakpoint, id: Int? = null) : + BreakPointInfo(breakpoint.line, breakpoint.sourcePosition!!.file, id) + + private class OneTimeBreakpointInfo(val position: XSourcePosition, id: Int? = null) : + BreakPointInfo(position.line, position.file, id) + + private val breakpoints = mutableListOf() + private val breakpointMap = mutableMapOf>() + private val breakpointsMapMutex = Mutex() + + private val editorsProvider = RobotCodeXDebuggerEditorsProvider() + private val breakpointHandler = RobotCodeBreakpointHandler(this) + + override fun getEditorsProvider(): XDebuggerEditorsProvider { + return editorsProvider + } + + override fun createConsole(): ExecutionConsole { + return executionResult.executionConsole + } + + override fun doGetProcessHandler(): ProcessHandler? { + return executionResult.processHandler + } + + override fun sessionInitialized() { + super.sessionInitialized() + } + + override fun getBreakpointHandlers(): Array?> { + return arrayOf(breakpointHandler) + } + + fun registerBreakpoint(breakpoint: XLineBreakpoint) { + runBlocking { + breakpointsMapMutex.withLock { + breakpoint.sourcePosition?.let { + if (!breakpointMap.containsKey(it.file)) { + breakpointMap[it.file] = mutableMapOf() + } + val bpMap = breakpointMap[it.file]!! + bpMap[breakpoint.line] = LineBreakpointInfo(breakpoint) + + sendBreakpointRequest(it.file) + } + } + } + } + + fun unregisterBreakpoint(breakpoint: XLineBreakpoint) { + runBlocking { + breakpointsMapMutex.withLock { + breakpoint.sourcePosition?.let { + if (breakpointMap.containsKey(it.file)) { + val bpMap = breakpointMap[it.file]!! + bpMap.remove(breakpoint.line) + + sendBreakpointRequest(it.file) + } + } + } + } + } + + private suspend fun sendBreakpointRequest() { + if (!state.isInitialized) return + + for (file in breakpointMap.keys) { + sendBreakpointRequest(file) + } + } + + private suspend fun sendBreakpointRequest(file: VirtualFile) { + if (!state.isInitialized) { + return + } + + val breakpoints = breakpointMap[file]!!.entries + if (breakpoints.isEmpty()) { + return + } + val arguments = SetBreakpointsArguments() + val source = Source() + source.path = file.toNioPath().toString() + arguments.source = source + + val dapBreakpoints = breakpoints.map { + val bp = it.value + SourceBreakpoint().apply { + line = bp.line + 1 + if (bp is LineBreakpointInfo) { + condition = bp.breakpoint.conditionExpression?.expression + logMessage = bp.breakpoint.logExpressionObject?.expression + } + } + } + + arguments.breakpoints = dapBreakpoints.toTypedArray() + + val response = debugServer.setBreakpoints(arguments).await() + + breakpoints.forEach { + val responseBreakpoint = response.breakpoints.firstOrNull { x -> x.line - 1 == it.value.line } + if (responseBreakpoint != null) { + it.value.id = responseBreakpoint.id + + (it.value as? LineBreakpointInfo)?.let { lineBreakpointInfo -> + if (responseBreakpoint.isVerified == true) { + session.setBreakpointVerified(lineBreakpointInfo.breakpoint) + } else { + session.setBreakpointInvalid(lineBreakpointInfo.breakpoint, "Invalid breakpoint") + } + } + } + } + } + + override fun stop() { + runBlocking { + try { + debugServer.terminate(TerminateArguments().apply { + restart = false + }).await() + } catch (_: Exception) { // Ignore may be the server is already terminated + } + } + } + + override fun resume(context: XSuspendContext?) { + runBlocking { + if (context is RobotCodeSuspendContext) { + debugServer.continue_(ContinueArguments().apply { + threadId = context.threadId + }).await() + } + } + } + + override fun startStepOver(context: XSuspendContext?) { + runBlocking { + if (context is RobotCodeSuspendContext) { + debugServer.next(NextArguments().apply { threadId = context.threadId }).await() + } + } + } + + override fun startStepInto(context: XSuspendContext?) { + runBlocking { + if (context is RobotCodeSuspendContext) { + debugServer.stepIn(StepInArguments().apply { + threadId = context.threadId + }).await() + } + } + } + + override fun startStepOut(context: XSuspendContext?) { + runBlocking { + if (context is RobotCodeSuspendContext) { + debugServer.stepOut(StepOutArguments().apply { + threadId = context.threadId + }).await() + } + } + } + + private var _oneTimeBreakpointInfo: OneTimeBreakpointInfo? = null + + private suspend fun removeCurrentOneTimeBreakpoint() { + _oneTimeBreakpointInfo?.let { + _oneTimeBreakpointInfo = null + breakpointMap[it.file]?.remove(it.line) + sendBreakpointRequest(it.file) + } + } + + override fun runToPosition(position: XSourcePosition, context: XSuspendContext?) { + runBlocking { + + if (!breakpointMap.containsKey(position.file)) { + breakpointMap[position.file] = mutableMapOf() + } + val bpMap = breakpointMap[position.file]!! + + removeCurrentOneTimeBreakpoint() + + if (bpMap.containsKey(position.line)) { + return@runBlocking + } + + _oneTimeBreakpointInfo = OneTimeBreakpointInfo(position) + bpMap[position.line] = OneTimeBreakpointInfo(position) + + sendBreakpointRequest(position.file) + + resume(context) + } + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProgramRunner.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProgramRunner.kt new file mode 100644 index 000000000..66ac281e1 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProgramRunner.kt @@ -0,0 +1,46 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.execution.configurations.RunProfile +import com.intellij.execution.configurations.RunProfileState +import com.intellij.execution.configurations.RunnerSettings +import com.intellij.execution.executors.DefaultDebugExecutor +import com.intellij.execution.runners.AsyncProgramRunner +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.ui.RunContentDescriptor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.XDebugProcessStarter +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerManager +import dev.robotcode.robotcode4ij.execution.RobotCodeRunConfiguration +import dev.robotcode.robotcode4ij.execution.RobotCodeRunProfileState +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.resolvedPromise + +class RobotCodeDebugProgramRunner : AsyncProgramRunner() { + override fun getRunnerId(): String { + return "dev.robotcode.robotcode4ij.execution.RobotCodeDebugProgramRunner" + } + + override fun canRun(executorId: String, profile: RunProfile): Boolean { + return (executorId == DefaultDebugExecutor.EXECUTOR_ID) && profile is RobotCodeRunConfiguration + } + + override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise { + FileDocumentManager.getInstance().saveAllDocuments() + + return resolvedPromise(doExecute(state as RobotCodeRunProfileState, environment)) + } + + private fun doExecute(state: RobotCodeRunProfileState, environment: ExecutionEnvironment): RunContentDescriptor { + val manager = XDebuggerManager.getInstance(environment.project) + val session = manager.startSession(environment, object : XDebugProcessStarter() { + override fun start(session: XDebugSession): XDebugProcess { + val result = state.execute(environment.executor, this@RobotCodeDebugProgramRunner) + + return RobotCodeDebugProcess(session, result, state) + } + }) + return session.runContentDescriptor + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProtocolClient.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProtocolClient.kt new file mode 100644 index 000000000..d1d041dfe --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebugProtocolClient.kt @@ -0,0 +1,206 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.openapi.diagnostic.thisLogger +import com.jetbrains.rd.util.reactive.Signal +import org.eclipse.lsp4j.debug.OutputEventArguments +import org.eclipse.lsp4j.debug.StoppedEventArguments +import org.eclipse.lsp4j.debug.TerminatedEventArguments +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification + +data class RobotEnqueuedArguments(var items: Array) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RobotEnqueuedArguments + + return items.contentEquals(other.items) + } + + override fun hashCode(): Int { + return items.contentHashCode() + } +} + +data class RobotExitedEventArguments( + var reportFile: String? = null, + var logFile: String? = null, + var outputFile: String? = null, + var exitCode: Int? = null +) + +data class RobotExecutionAttributes( + var id: String? = null, + var parentId: String? = null, + var longname: String? = null, + var template: String? = null, + var status: String? = null, + var message: String? = null, + var elapsedtime: Int? = null, + var source: String? = null, + var lineno: Int? = null, + var starttime: String? = null, + var endtime: String? = null, + var tags: Array? = null, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RobotExecutionAttributes + + if (elapsedtime != other.elapsedtime) return false + if (lineno != other.lineno) return false + if (id != other.id) return false + if (parentId != other.parentId) return false + if (longname != other.longname) return false + if (template != other.template) return false + if (status != other.status) return false + if (message != other.message) return false + if (source != other.source) return false + if (starttime != other.starttime) return false + if (endtime != other.endtime) return false + if (tags != null) { + if (other.tags == null) return false + if (!tags.contentEquals(other.tags)) return false + } else if (other.tags != null) return false + + return true + } + + override fun hashCode(): Int { + var result = elapsedtime?.hashCode() ?: 0 + result = 31 * result + (lineno ?: 0) + result = 31 * result + (id?.hashCode() ?: 0) + result = 31 * result + (parentId?.hashCode() ?: 0) + result = 31 * result + (longname?.hashCode() ?: 0) + result = 31 * result + (template?.hashCode() ?: 0) + result = 31 * result + (status?.hashCode() ?: 0) + result = 31 * result + (message?.hashCode() ?: 0) + result = 31 * result + (source?.hashCode() ?: 0) + result = 31 * result + (starttime?.hashCode() ?: 0) + result = 31 * result + (endtime?.hashCode() ?: 0) + result = 31 * result + (tags?.contentHashCode() ?: 0) + return result + } +} + +data class RobotExecutionEventArguments( + var type: String, + var id: String, + var name: String, + var parentId: String? = null, + var attributes: RobotExecutionAttributes, + var failedKeywords: Array? = null, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RobotExecutionEventArguments + + if (type != other.type) return false + if (id != other.id) return false + if (attributes != other.attributes) return false + if (failedKeywords != null) { + if (other.failedKeywords == null) return false + if (!failedKeywords.contentEquals(other.failedKeywords)) return false + } else if (other.failedKeywords != null) return false + + return true + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + id.hashCode() + result = 31 * result + attributes.hashCode() + result = 31 * result + (failedKeywords?.contentHashCode() ?: 0) + return result + } +} + +enum class RobotLogLevel { + FAIL, + ERROR, + WARN, + INFO, + DEBUG, + TRACE +} + +data class RobotLogMessageEventArguments( + var itemId: String? = null, + var source: String? = null, + var lineno: Int? = null, + var column: Int? = null, + var message: String? = null, + var level: String? = null, + var timestamp: String? = null, + var html: String? = null +) + +@Suppress("unused") class RobotCodeDebugProtocolClient : IDebugProtocolClient { + var onStopped = Signal() + val onTerminated = Signal() + + val onRobotEnqueued = Signal() + val onRobotStarted = Signal() + val onRobotEnded = Signal() + val onRobotSetFailed = Signal() + val onRobotExited = Signal() + val onRobotLog = Signal() + val onRobotMessage = Signal() + val onOutput = Signal() + + override fun terminated(args: TerminatedEventArguments?) { + super.terminated(args) + onTerminated.fire(args) + } + + override fun stopped(args: StoppedEventArguments) { + super.stopped(args) + onStopped.fire(args) + } + + @JsonNotification("robotEnqueued") fun robotEnqueued(args: RobotEnqueuedArguments) { + thisLogger().trace("robotEnqueued") + onRobotEnqueued.fire(args) + } + + @JsonNotification("robotStarted") fun robotStarted(args: RobotExecutionEventArguments) { + thisLogger().trace("robotStarted $args") + onRobotStarted.fire(args) + } + + @JsonNotification("robotEnded") fun robotEnded(args: RobotExecutionEventArguments) { + thisLogger().trace("robotEnded $args") + onRobotEnded.fire(args) + } + + @JsonNotification("robotSetFailed") fun robotSetFailed(args: RobotExecutionEventArguments) { + thisLogger().trace("robotSetFailed $args") + onRobotSetFailed.fire(args) + } + + @JsonNotification("robotExited") fun robotExited(args: RobotExitedEventArguments) { + thisLogger().trace("robotExited") + onRobotExited.fire(args) + } + + @JsonNotification("robotLog") fun robotLog(args: RobotLogMessageEventArguments) { + thisLogger().trace("robotLog") + onRobotLog.fire(args) + } + + @JsonNotification("robotMessage") fun robotMessage(args: RobotLogMessageEventArguments) { + thisLogger().trace("robotMessage") + onRobotMessage.fire(args) + } + + override fun output(args: OutputEventArguments) { + super.output(args) + onOutput.fire(args) + } + +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebuggerEvaluator.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebuggerEvaluator.kt new file mode 100644 index 000000000..8ae7b9b2a --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeDebuggerEvaluator.kt @@ -0,0 +1,68 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.openapi.editor.Document +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiFile +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.evaluation.EvaluationMode +import com.intellij.xdebugger.evaluation.ExpressionInfo +import com.intellij.xdebugger.evaluation.XDebuggerEvaluator +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.eclipse.lsp4j.debug.EvaluateArguments +import org.eclipse.lsp4j.debug.Variable +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer + +class RobotCodeDebuggerEvaluator(val debugServer: IDebugProtocolServer, val frame: RobotCodeStackFrame) : + XDebuggerEvaluator() { + override fun evaluate( + expression: String, + callback: XEvaluationCallback, + expressionPosition: XSourcePosition? + ) { + runBlocking { + val result = debugServer.evaluate(EvaluateArguments().apply { + this.expression = expression + frameId = frame.frame.id + }).await() + val variable: Variable = Variable().apply { + value = result.result + evaluateName = expression + variablesReference = result.variablesReference + type = result.type + presentationHint = result.presentationHint + } + callback.evaluated( + RobotCodeNamedValue( + variable, + null, + debugServer, + frame.session + ) + ) + } + } + + override fun getEvaluationMode(text: String, startOffset: Int, endOffset: Int, psiFile: PsiFile?): EvaluationMode? { + return super.getEvaluationMode(text, startOffset, endOffset, psiFile) + } + + override fun getExpressionInfoAtOffset( + project: Project, + document: Document, + offset: Int, + sideEffectsAllowed: Boolean + ): ExpressionInfo? { + return super.getExpressionInfoAtOffset(project, document, offset, sideEffectsAllowed) + } + + override fun getExpressionRangeAtOffset( + project: Project?, + document: Document?, + offset: Int, + sideEffectsAllowed: Boolean + ): TextRange? { + return super.getExpressionRangeAtOffset(project, document, offset, sideEffectsAllowed) + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeExecutionStack.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeExecutionStack.kt new file mode 100644 index 000000000..0eff9ea7d --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeExecutionStack.kt @@ -0,0 +1,38 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.frame.XExecutionStack +import com.intellij.xdebugger.frame.XStackFrame +import org.eclipse.lsp4j.debug.StackTraceResponse +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer + +@Suppress("DialogTitleCapitalization") class RobotCodeExecutionStack( + val stack: StackTraceResponse, + val debugServer: IDebugProtocolServer, + val threadId: Int, + val session: XDebugSession +) : + XExecutionStack("Robot Framework Execution Stack") { + override fun getTopFrame(): XStackFrame? { + return stack.stackFrames.firstOrNull()?.let { + RobotCodeStackFrame( + it, + debugServer, + session + ) + } + } + + override fun computeStackFrames( + firstFrameIndex: Int, container: XStackFrameContainer? + ) { + container?.addStackFrames(stack.stackFrames.drop(firstFrameIndex).map { + RobotCodeStackFrame( + it, + debugServer, + session + ) + }, true) + } + +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeNamedValue.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeNamedValue.kt new file mode 100644 index 000000000..4b8531b01 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeNamedValue.kt @@ -0,0 +1,53 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.icons.AllIcons +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XNamedValue +import com.intellij.xdebugger.frame.XValueChildrenList +import com.intellij.xdebugger.frame.XValueNode +import com.intellij.xdebugger.frame.XValuePlace +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.eclipse.lsp4j.debug.Variable +import org.eclipse.lsp4j.debug.VariablesArguments +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer + +class RobotCodeNamedValue( + val variable: Variable, val variableRef: Int?, val debugServer: IDebugProtocolServer, val session: XDebugSession +) : XNamedValue(variable.name ?: "") { + + init { + session.suspendContext?.let { + val variablesCache = (session.suspendContext as RobotCodeSuspendContext).variablesCache + variablesCache.getOrDefault((Pair(variableRef, variable.name)), null)?.let { + variable.value = it.value + variable.type = it.type ?: variable.type + variable.variablesReference = variable.variablesReference + variable.namedVariables = it.namedVariables + variable.indexedVariables = it.indexedVariables + } + } + } + + override fun computePresentation(node: XValueNode, place: XValuePlace) { + node.setPresentation(AllIcons.Nodes.Variable, variable.type, variable.value, variable.variablesReference != 0) + } + + override fun computeChildren(node: XCompositeNode) { + runBlocking { + if (variable.variablesReference != 0) { + val list = XValueChildrenList() + debugServer.variables(VariablesArguments().apply { variablesReference = variable.variablesReference }) + .await().variables.forEach { + list.add( + it.name, RobotCodeNamedValue( + it, variable.variablesReference, debugServer, session + ) + ) + } + node.addChildren(list, true) + } + } + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeStackFrame.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeStackFrame.kt new file mode 100644 index 000000000..62459f4b9 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeStackFrame.kt @@ -0,0 +1,75 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.ui.ColoredTextContainer +import com.intellij.ui.SimpleTextAttributes +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerUtil +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.evaluation.XDebuggerEvaluator +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XStackFrame +import com.intellij.xdebugger.frame.XValueChildrenList +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import org.eclipse.lsp4j.debug.ScopesArguments +import org.eclipse.lsp4j.debug.StackFrame +import org.eclipse.lsp4j.debug.VariablesArguments +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer +import kotlin.io.path.Path + +class RobotCodeStackFrame(val frame: StackFrame, val debugServer: IDebugProtocolServer, val session: XDebugSession) : + XStackFrame() { + override fun getSourcePosition(): XSourcePosition? { + val file = VfsUtil.findFile(Path(frame.source?.path ?: return null), false) + return XDebuggerUtil.getInstance().createPosition(file, frame.line - 1, frame.column) + } + + override fun getEvaluator(): XDebuggerEvaluator { + return RobotCodeDebuggerEvaluator(debugServer, this) + } + + override fun customizePresentation(component: ColoredTextContainer) { + if (frame.source == null) { + component.append(frame.name.orEmpty(), SimpleTextAttributes.REGULAR_ATTRIBUTES) + } else { + super.customizePresentation(component) + } + } + + override fun computeChildren(node: XCompositeNode) { + // TODO: Implement this method + runBlocking { + val scopesResponse = debugServer.scopes(ScopesArguments().apply { frameId = frame.id }).await() + val list = XValueChildrenList() + val localScope = scopesResponse.scopes.first { scope -> scope.name.lowercase() == "local" } + val localVariables = debugServer.variables(VariablesArguments().apply { + variablesReference = localScope.variablesReference + }).await() + localVariables.variables.forEach { + list.add( + it.name, + RobotCodeNamedValue( + it, + localScope.variablesReference, + debugServer, + session + ) + ) + } + + scopesResponse.scopes.filter { x -> x.name.lowercase() != "local" }.forEach { + val variableRef = it.variablesReference + val groupName = it.name + val variables = debugServer.variables(VariablesArguments().apply { + variablesReference = variableRef + }).await() + val group = + RobotCodeValueGroup(groupName, variables, variableRef, debugServer, session) + list.addBottomGroup(group) + } + + node.addChildren(list, true) + } + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeSuspendContext.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeSuspendContext.kt new file mode 100644 index 000000000..804e63611 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeSuspendContext.kt @@ -0,0 +1,26 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.frame.XExecutionStack +import com.intellij.xdebugger.frame.XSuspendContext +import org.eclipse.lsp4j.debug.StackTraceResponse +import org.eclipse.lsp4j.debug.Variable +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer + +class RobotCodeSuspendContext( + val stack: StackTraceResponse, + val threadId: Int, + val debugServer: IDebugProtocolServer, + val session: XDebugSession +) : XSuspendContext() { + + val variablesCache: MutableMap, Variable> = mutableMapOf() + + override fun getExecutionStacks(): Array { + return arrayOf(RobotCodeExecutionStack(stack, debugServer, threadId, session)) + } + + override fun getActiveExecutionStack(): XExecutionStack { + return RobotCodeExecutionStack(stack, debugServer, threadId, session) + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeValueGroup.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeValueGroup.kt new file mode 100644 index 000000000..3370c2a44 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeValueGroup.kt @@ -0,0 +1,27 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XValueChildrenList +import com.intellij.xdebugger.frame.XValueGroup +import org.eclipse.lsp4j.debug.VariablesResponse +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer + +class RobotCodeValueGroup( + groupName: String, + val variables: VariablesResponse, + val variableRef: Int, + val debugServer: IDebugProtocolServer, + val session: XDebugSession, +) : XValueGroup(groupName) { + override fun computeChildren(node: XCompositeNode) { + val list = XValueChildrenList() + variables.variables.forEach { + list.add( + it.name, + RobotCodeNamedValue(it, variableRef, debugServer, session) + ) + } + node.addChildren(list, true) + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeXDebuggerEditorsProvider.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeXDebuggerEditorsProvider.kt new file mode 100644 index 000000000..d45d7d765 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/debugging/RobotCodeXDebuggerEditorsProvider.kt @@ -0,0 +1,25 @@ +package dev.robotcode.robotcode4ij.debugging + +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.xdebugger.evaluation.XDebuggerEditorsProviderBase +import dev.robotcode.robotcode4ij.RobotSuiteFileType + +class RobotCodeXDebuggerEditorsProvider : XDebuggerEditorsProviderBase() { + override fun getFileType(): FileType { + return RobotSuiteFileType + } + + override fun createExpressionCodeFragment( + project: Project, text: String, context: PsiElement?, isPhysical: Boolean + ): PsiFile { + val fileName = context?.containingFile?.name ?: "dummy.robot" + return PsiFileFactory.getInstance(project)!!.createFileFromText( + fileName, RobotSuiteFileType, text + ) + } + +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeProgramRunner.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeProgramRunner.kt index 411e5e5e0..10d5f8857 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeProgramRunner.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeProgramRunner.kt @@ -3,7 +3,6 @@ package dev.robotcode.robotcode4ij.execution import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.configurations.RunnerSettings -import com.intellij.execution.executors.DefaultDebugExecutor import com.intellij.execution.executors.DefaultRunExecutor import com.intellij.execution.runners.AsyncProgramRunner import com.intellij.execution.runners.ExecutionEnvironment @@ -15,20 +14,20 @@ import org.jetbrains.concurrency.resolvedPromise class RobotCodeProgramRunner : AsyncProgramRunner() { override fun getRunnerId(): String { - return "robotCodeProgramRunner" + return "dev.robotcode.robotcode4ij.execution.RobotCodeProgramRunner" } override fun canRun(executorId: String, profile: RunProfile): Boolean { - return (executorId == DefaultRunExecutor.EXECUTOR_ID || executorId == DefaultDebugExecutor.EXECUTOR_ID) && profile is RobotCodeRunConfiguration + return (executorId == DefaultRunExecutor.EXECUTOR_ID) && profile is RobotCodeRunConfiguration } override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise { FileDocumentManager.getInstance().saveAllDocuments() - return resolvedPromise(doExecute(state, environment)) + return resolvedPromise(doExecute(state as RobotCodeRunProfileState, environment)) } - private fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? { + private fun doExecute(state: RobotCodeRunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? { return showRunContent(state.execute(environment.executor, this), environment) } } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfiguration.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfiguration.kt index bbb60a411..693a0dac7 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfiguration.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfiguration.kt @@ -1,20 +1,23 @@ package dev.robotcode.robotcode4ij.execution import com.intellij.execution.Executor -import com.intellij.execution.compound.CompoundRunConfigurationSettingsEditor import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.LocatableConfigurationBase import com.intellij.execution.configurations.RunConfiguration import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.testframework.sm.runner.SMRunnerConsolePropertiesProvider +import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project +import dev.robotcode.robotcode4ij.testing.RobotCodeTestItem class RobotCodeRunConfiguration(project: Project, factory: ConfigurationFactory) : LocatableConfigurationBase - (project, factory, "Robot Framework") { + (project, factory, "Robot Framework"), SMRunnerConsolePropertiesProvider { + override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState { - return RobotCodeRunProfileState(environment) + return RobotCodeRunProfileState(this, environment) } override fun getConfigurationEditor(): SettingsEditor { @@ -22,6 +25,11 @@ class RobotCodeRunConfiguration(project: Project, factory: ConfigurationFactory) return RobotCodeRunConfigurationEditor() } - var suite: String = "" - var test: String = "" + var includedTestItems: List = emptyList() + + var paths: List = emptyList() + + override fun createTestConsoleProperties(executor: Executor): SMTRunnerConsoleProperties { + return RobotRunnerConsoleProperties(this, "Robot Framework", executor) + } } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationEditor.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationEditor.kt index 831a6f511..5fe189ad9 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationEditor.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationEditor.kt @@ -1,11 +1,11 @@ package dev.robotcode.robotcode4ij.execution import com.intellij.execution.configuration.EnvironmentVariablesComponent -import com.intellij.util.ui.ComponentWithEmptyText -import com.intellij.ui.RawCommandLineEditor import com.intellij.openapi.options.SettingsEditor +import com.intellij.ui.RawCommandLineEditor import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.panel +import com.intellij.util.ui.ComponentWithEmptyText import javax.swing.JComponent class RobotCodeRunConfigurationEditor : SettingsEditor() { diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationProducer.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationProducer.kt index 4d85aabed..ba0171093 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationProducer.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunConfigurationProducer.kt @@ -6,10 +6,7 @@ import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.runConfigurationType import com.intellij.openapi.util.Ref import com.intellij.psi.PsiElement -import com.intellij.psi.util.elementType -import dev.robotcode.robotcode4ij.psi.FILE -import dev.robotcode.robotcode4ij.psi.RobotSuiteFile -import dev.robotcode.robotcode4ij.psi.TESTCASE_NAME +import dev.robotcode.robotcode4ij.testing.testManger class RobotCodeRunConfigurationProducer : LazyRunConfigurationProducer() { @@ -22,47 +19,22 @@ class RobotCodeRunConfigurationProducer : LazyRunConfigurationProducer ): Boolean { - // TODO - val psiElement = sourceElement.get() - val psiFile = psiElement.containingFile as? RobotSuiteFile ?: return false - val virtualFile = psiFile.virtualFile ?: return false + val testItem = configuration.project.testManger.findTestItem(sourceElement.get()) ?: return false - when (psiElement.elementType) { - TESTCASE_NAME -> { - configuration.name = psiElement.text - configuration.suite = virtualFile.url - configuration.test = psiElement.text - return true - } - - FILE -> { - configuration.name = virtualFile.presentableName - configuration.suite = virtualFile.url - return true - } - - else -> return false - } + configuration.name = testItem.name + configuration.includedTestItems = listOf(testItem) + + return true } override fun isConfigurationFromContext( configuration: RobotCodeRunConfiguration, context: ConfigurationContext ): Boolean { - val psiElement = context.psiLocation - val psiFile = psiElement?.containingFile as? RobotSuiteFile ?: return false - val virtualFile = psiFile.virtualFile ?: return false - return when (psiElement.elementType) { - TESTCASE_NAME -> { - configuration.suite == virtualFile.url && configuration.test == psiElement.text - } - - FILE -> { - configuration.suite == virtualFile.url - } - - else -> false - } + val psiElement = context.psiLocation ?: return false + val testItem = configuration.project.testManger.findTestItem(psiElement) ?: return false + + return configuration.includedTestItems == listOf(testItem) } } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunLineMarkerContributor.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunLineMarkerContributor.kt index a6c1e1014..729f5adea 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunLineMarkerContributor.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunLineMarkerContributor.kt @@ -3,14 +3,19 @@ package dev.robotcode.robotcode4ij.execution import com.intellij.execution.lineMarker.RunLineMarkerContributor import com.intellij.icons.AllIcons import com.intellij.psi.PsiElement -import com.intellij.psi.util.elementType -import dev.robotcode.robotcode4ij.psi.FILE -import dev.robotcode.robotcode4ij.psi.TESTCASE_NAME +import dev.robotcode.robotcode4ij.testing.testManger class RobotCodeRunLineMarkerContributor : RunLineMarkerContributor() { override fun getInfo(element: PsiElement): Info? { - if (element.elementType != TESTCASE_NAME && element.elementType != FILE) return null - - return withExecutorActions(AllIcons.RunConfigurations.TestState.Run) + var testElement = element.project.testManger.findTestItem(element) ?: return null + var icon = AllIcons.RunConfigurations.TestState.Run + if (testElement.type == "suite") { + icon = AllIcons.RunConfigurations.TestState.Run_run + } + return withExecutorActions(icon) + } + + override fun getSlowInfo(element: PsiElement): Info? { + return super.getSlowInfo(element) } } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunProfileState.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunProfileState.kt index e770e4747..5f626624c 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunProfileState.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunProfileState.kt @@ -1,24 +1,231 @@ package dev.robotcode.robotcode4ij.execution +import com.intellij.execution.CantRunException +import com.intellij.execution.DefaultExecutionResult +import com.intellij.execution.ExecutionException +import com.intellij.execution.ExecutionResult +import com.intellij.execution.Executor import com.intellij.execution.configurations.CommandLineState -import com.intellij.execution.process.KillableProcessHandler +import com.intellij.execution.process.KillableColoredProcessHandler +import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessHandler +import com.intellij.execution.process.ProcessListener import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.runners.ProgramRunner +import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil.createAndAttachConsole +import com.intellij.execution.testframework.sm.runner.SMRunnerConsolePropertiesProvider +import com.intellij.execution.ui.ConsoleView +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.Ref +import com.jetbrains.rd.util.reactive.Signal +import com.jetbrains.rd.util.reactive.adviseEternal import dev.robotcode.robotcode4ij.buildRobotCodeCommandLine +import dev.robotcode.robotcode4ij.debugging.RobotCodeDebugProgramRunner +import dev.robotcode.robotcode4ij.debugging.RobotCodeDebugProtocolClient +import dev.robotcode.robotcode4ij.utils.NetUtils.findFreePort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.eclipse.lsp4j.debug.ConfigurationDoneArguments +import org.eclipse.lsp4j.debug.InitializeRequestArguments +import org.eclipse.lsp4j.debug.launch.DSPLauncher +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer +import org.eclipse.lsp4j.jsonrpc.Launcher +import java.net.Socket +import java.net.SocketTimeoutException +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid -class RobotCodeRunProfileState(environment: ExecutionEnvironment) : CommandLineState(environment) { + +class RobotCodeRunProfileState(private val config: RobotCodeRunConfiguration, environment: ExecutionEnvironment) : + CommandLineState(environment), ProcessListener { + + companion object { + const val DEBUGGER_DEFAULT_PORT = 6612 + val DEBUG_PORT: Key = Key.create("ROBOTCODE_DEBUG_PORT") + } + + val debugClient = RobotCodeDebugProtocolClient() + lateinit var debugServer: IDebugProtocolServer + var isInitialized = false + private set + var isConfigurationDone = false + private set + + val afterInitialize = Signal() + val afterConfigurationDone = Signal() + + + init { + debugClient.onTerminated.adviseEternal { + if (socket.isConnected) socket.close() + } + } + + private lateinit var socket: Socket + override fun startProcess(): ProcessHandler { val project = environment.project - val profile = environment.runProfile as? RobotCodeRunConfiguration + val profile = + environment.runProfile as? RobotCodeRunConfiguration ?: throw CantRunException("Invalid run configuration") + // TODO: Add support for configurable paths val defaultPaths = arrayOf("--default-path", ".") - val commandLine = project.buildRobotCodeCommandLine(arrayOf(*defaultPaths, "run")) + val debug = environment.runner is RobotCodeDebugProgramRunner + + val included = mutableListOf() + for (test in profile.includedTestItems) { + included.add("--by-longname") + included.add(test.longname) + } + + val connection = mutableListOf() + + val port = findFreePort(DEBUGGER_DEFAULT_PORT) + if (port != DEBUGGER_DEFAULT_PORT) { + included.add("--tcp") + included.add(port.toString()) + } - val handler = KillableProcessHandler(commandLine) + val commandLine = project.buildRobotCodeCommandLine( + arrayOf( + *defaultPaths, + "debug", + *connection.toTypedArray(), + *(if (!debug) arrayOf("--no-debug") else arrayOf()), + *(included.toTypedArray()) + ), + noColor = false + // extraArgs = arrayOf("-v", "--log", "--log-level", "TRACE") + + ) + + val handler = KillableColoredProcessHandler(commandLine) + // handler.setHasPty(true) + handler.putUserData(DEBUG_PORT, port) ProcessTerminatedListener.attach(handler) + handler.addProcessListener(this) return handler } + override fun execute(executor: Executor, runner: ProgramRunner<*>): ExecutionResult { + val processHandler = startProcess() + val console: ConsoleView = createAndAttachConsoleInEDT(processHandler, executor) + return DefaultExecutionResult(console, processHandler, *createActions(console, processHandler)) + } + + private fun createAndAttachConsoleInEDT(processHandler: ProcessHandler, executor: Executor): ConsoleView { + val consoleRef = Ref.create() + ApplicationManager.getApplication().invokeAndWait { + try { + val properties = config as? SMRunnerConsolePropertiesProvider + if (properties == null) { + consoleRef.set(super.createConsole(executor)) + } else { + val consoleProperties = properties.createTestConsoleProperties(executor) + if (consoleProperties is RobotRunnerConsoleProperties) { + consoleProperties.state = this + } + consoleRef.set( + createAndAttachConsole( + "RobotCode", processHandler, consoleProperties + ) + ) + } + } catch (e: ExecutionException) { + consoleRef.set(e) + } catch (e: RuntimeException) { + consoleRef.set(e) + } + } + + if (consoleRef.get() is ExecutionException) { + throw consoleRef.get() as ExecutionException + } else if (consoleRef.get() is RuntimeException) throw consoleRef.get() as RuntimeException + + return consoleRef.get() as ConsoleView + } + + private suspend fun tryConnectToServerWithTimeout( + host: String, port: Int, timeoutMillis: Long, retryIntervalMillis: Long + ): Socket? { + return try { + withTimeout(timeoutMillis) { + var socket: Socket? = null + while (socket == null || !socket.isConnected) { + socket = null + try { + socket = withContext(Dispatchers.IO) { + Socket(host, port) + } + } catch (_: SocketTimeoutException) { + } catch (_: Exception) { + } + delay(retryIntervalMillis) + + } + socket + } + } catch (e: TimeoutCancellationException) { + null + } + } + + @OptIn(ExperimentalUuidApi::class) override fun startNotified(event: ProcessEvent) { + runBlocking(Dispatchers.IO) { + + var port = event.processHandler.getUserData(DEBUG_PORT) ?: throw CantRunException("No debug port found.") + + socket = tryConnectToServerWithTimeout("127.0.0.1", port, 10000, retryIntervalMillis = 100) + ?: throw CantRunException("Unable to establish connection to debug server.") + + val launcher: Launcher = + DSPLauncher.createClientLauncher(debugClient, socket.getInputStream(), socket.getOutputStream()) + + launcher.startListening() + + debugServer = launcher.remoteProxy + + val arguments = InitializeRequestArguments().apply { + clientID = Uuid.random().toString() + adapterID = Uuid.random().toString() + + clientName = "RobotCode4IJ" + locale = "en_US" + + supportsRunInTerminalRequest = false + supportsStartDebuggingRequest = false + pathFormat = "path" + supportsVariableType = true + supportsVariablePaging = false + + linesStartAt1 = true + columnsStartAt1 = true + } + + val response = debugServer.initialize(arguments).await() + isInitialized = true + + afterInitialize.fire(Unit) + + if (response.supportsConfigurationDoneRequest) { + debugServer.configurationDone(ConfigurationDoneArguments()).await() + isConfigurationDone = true + } + + afterConfigurationDone.fire(Unit) + debugServer.attach(emptyMap()) + } + } + + override fun processTerminated(event: ProcessEvent) { + if (socket.isConnected) socket.close() + } } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotOutputToGeneralTestEventsConverter.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotOutputToGeneralTestEventsConverter.kt new file mode 100644 index 000000000..787ae8a41 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotOutputToGeneralTestEventsConverter.kt @@ -0,0 +1,149 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.CantRunException +import com.intellij.execution.testframework.sm.ServiceMessageBuilder +import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.util.Key +import com.intellij.util.Urls.newLocalFileUrl +import com.intellij.util.Urls.newUrl +import com.jetbrains.rd.util.reactive.adviseEternal +import dev.robotcode.robotcode4ij.debugging.RobotExecutionEventArguments +import jetbrains.buildServer.messages.serviceMessages.ServiceMessage +import jetbrains.buildServer.messages.serviceMessages.ServiceMessageVisitor +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.eclipse.lsp4j.debug.OutputEventArgumentsCategory + +class RobotOutputToGeneralTestEventsConverter( + testFrameworkName: String, consoleProperties: RobotRunnerConsoleProperties, +) : OutputToGeneralTestEventsConverter(testFrameworkName, consoleProperties) { + + private var _firstCall = false + private lateinit var visitor: ServiceMessageVisitor + private val testItemIdStack = mutableListOf() + + private fun robotStarted(args: RobotExecutionEventArguments) { + testItemIdStack.add(args.id) + + val msg = when (args.type) { + "suite" -> ServiceMessageBuilder.testSuiteStarted(args.name) + "test" -> ServiceMessageBuilder.testStarted(args.name) + else -> null + } + + processRobotMessage(msg, args) + } + + private fun robotEnded(args: RobotExecutionEventArguments) { + val msg = when (args.type) { + "suite" -> ServiceMessageBuilder.testSuiteFinished(args.name) + "test" -> when (args.attributes.status) { + "PASS" -> ServiceMessageBuilder.testFinished(args.name).apply { + if (args.attributes.message != null) { + addAttribute("message", args.attributes.message) + } + } + + "SKIP" -> ServiceMessageBuilder.testIgnored(args.name).apply { + addAttribute("message", args.attributes.message ?: "Skipped") + } + + else -> ServiceMessageBuilder.testFailed(args.name).apply { + addAttribute("message", args.attributes.message ?: "Error") + } + + } + + else -> null + } + + processRobotMessage(msg, args) + + val lastId = testItemIdStack.removeLast() + if (lastId != args.id) { + thisLogger().warn("Test item ID stack is out of sync. Expected $lastId, got ${args.id}") + } + } + + private fun processRobotMessage(msg: ServiceMessageBuilder?, args: RobotExecutionEventArguments) { + if (msg != null) { + + with(msg) { + addAttribute("nodeId", args.id) + addAttribute("parentNodeId", args.parentId ?: "0") + if (args.attributes.source != null) { + val uri = newUrl( + "robotcode", "/", newLocalFileUrl(args.attributes.source!!).toString() + ).addParameters(mapOf("line" to ((args.attributes.lineno ?: 1) - 1).toString())) + + addAttribute("locationHint", uri.toString()) + } + addAttribute("duration", (args.attributes.elapsedtime ?: 0).toString()).toString() + } + + this.processServiceMessageFromRobot(msg) + } + } + + private var configurationDone = CompletableDeferred() + + private fun processConnected() { + configurationDone.complete(Unit) + } + + init { + consoleProperties.state?.afterInitialize?.adviseEternal { + runBlocking { + try { + withTimeout(5000) { + configurationDone.await() + } + } catch (e: TimeoutCancellationException) { + throw CantRunException("Configuration done request timed out.", e) + } + } + } + consoleProperties.state?.debugClient?.onRobotStarted?.adviseEternal(this::robotStarted) + consoleProperties.state?.debugClient?.onRobotEnded?.adviseEternal(this::robotEnded) + consoleProperties.state?.debugClient?.onRobotLog?.adviseEternal { args -> // TODO: Implement this + // val msg = ServiceMessageBuilder.testStdOut("blah") + // + // msg.addAttribute("nodeId", args.itemId ?: "0").addAttribute( + // "out", "[${args.level}] ${args.message}\n" + // ) + // this.processServiceMessageFromRobot(msg) + } + consoleProperties.state?.debugClient?.onOutput?.adviseEternal { args -> + val msg = + if (args.category == OutputEventArgumentsCategory.STDERR) ServiceMessageBuilder.testStdErr(args.category) + else ServiceMessageBuilder.testStdOut(args.category) + + msg.addAttribute("nodeId", testItemIdStack.lastOrNull() ?: "0") + msg.addAttribute("out", "${args.output}") + + this.processServiceMessageFromRobot(msg) + } + + } + + private fun processServiceMessageFromRobot(msg: ServiceMessageBuilder) { + ServiceMessage.parse(msg.toString())?.let { + this.processServiceMessage(it, visitor) + } + } + + override fun processServiceMessages(text: String, outputType: Key<*>, visitor: ServiceMessageVisitor): Boolean { + if (!_firstCall) { + _firstCall = true + this.visitor = visitor + + processConnected() + } + + // TODO: make this configurable + return true + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotRunnerConsoleProperties.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotRunnerConsoleProperties.kt new file mode 100644 index 000000000..b0d17cb03 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotRunnerConsoleProperties.kt @@ -0,0 +1,44 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.Executor +import com.intellij.execution.testframework.TestConsoleProperties +import com.intellij.execution.testframework.sm.SMCustomMessagesParsing +import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter +import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties +import com.intellij.execution.testframework.sm.runner.SMTestLocator + +class RobotRunnerConsoleProperties( + config: RobotCodeRunConfiguration, testFrameworkName: String, executor: Executor +) : SMTRunnerConsoleProperties(config, testFrameworkName, executor), SMCustomMessagesParsing { + + var state: RobotCodeRunProfileState? = null + + init { // isUsePredefinedMessageFilter = false + setIfUndefined(HIDE_PASSED_TESTS, false) + setIfUndefined(HIDE_IGNORED_TEST, true) + setIfUndefined(SCROLL_TO_SOURCE, true) + setIfUndefined(SELECT_FIRST_DEFECT, true) + setIfUndefined(SHOW_STATISTICS, true) + + isIdBasedTestTree = true + isPrintTestingStartedTime = true + } + + override fun getTestLocator(): SMTestLocator { + return RobotSMTestLocator() + } + + override fun createTestEventsConverter( + testFrameworkName: String, consoleProperties: TestConsoleProperties + ): OutputToGeneralTestEventsConverter { + if (consoleProperties !is RobotRunnerConsoleProperties) { + return OutputToGeneralTestEventsConverter(testFrameworkName, consoleProperties) + } + return RobotOutputToGeneralTestEventsConverter(testFrameworkName, consoleProperties) + } + + override fun isEditable(): Boolean { + return true + } +} + diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotSMTestLocator.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotSMTestLocator.kt new file mode 100644 index 000000000..fc20f3d7d --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotSMTestLocator.kt @@ -0,0 +1,39 @@ +package dev.robotcode.robotcode4ij.execution + +import com.intellij.execution.Location +import com.intellij.execution.PsiLocation +import com.intellij.execution.testframework.sm.runner.SMTestLocator +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.Urls + +class RobotSMTestLocator : SMTestLocator { + override fun getLocation( + protocol: String, path: String, project: Project, scope: GlobalSearchScope + ): MutableList> { + + val uri = Urls.parse("file://$path", true) + if (uri != null) { + val line = uri.parameters?.drop(1)?.split("&")?.firstOrNull { it.startsWith("line=") }?.substring(5) + ?.toIntOrNull() + + LocalFileSystem.getInstance().findFileByPath(uri.path)?.let { + PsiManager.getInstance(project).findFile(it)?.let { + val document = PsiDocumentManager.getInstance(project).getDocument(it) + document?.let { doc -> + val offset = doc.getLineStartOffset(line ?: 0) + it.findElementAt(offset) + } + } + }?.let { + return mutableListOf(PsiLocation(it)) + } + } + return mutableListOf() + } + +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerFactory.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerFactory.kt index 274856490..23aed4335 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerFactory.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerFactory.kt @@ -9,7 +9,8 @@ import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider import dev.robotcode.robotcode4ij.lsp.RobotCodeLanguageServerManager.Companion.LANGUAGE_SERVER_ENABLED_KEY import dev.robotcode.robotcode4ij.lsp.features.RobotDiagnosticsFeature -class RobotCodeLanguageServerFactory : LanguageServerFactory, LanguageServerEnablementSupport { +@Suppress("UnstableApiUsage") class RobotCodeLanguageServerFactory : LanguageServerFactory, + LanguageServerEnablementSupport { override fun createConnectionProvider(project: Project): StreamConnectionProvider { return RobotCodeLanguageServer(project) } @@ -17,6 +18,7 @@ class RobotCodeLanguageServerFactory : LanguageServerFactory, LanguageServerEnab override fun createClientFeatures(): LSPClientFeatures { return super.createClientFeatures().setDiagnosticFeature(RobotDiagnosticsFeature()) } + override fun createLanguageClient(project: Project): LanguageClientImpl { return RobotCodeLanguageClient(project) } diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt index e96e08c8e..3b79d2032 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/lsp/RobotCodeLanguageServerManager.kt @@ -38,7 +38,7 @@ class RobotCodeLanguageServerManager(private val project: Project) { if (tryConfigureProject()) { val options = LanguageServerManager.StartOptions() - options.setForceStart(true) + options.isForceStart = true LanguageServerManager.getInstance(project).start(LANGUAGE_SERVER_ID, options) LanguageServerManager.getInstance(project).getLanguageServer(LANGUAGE_SERVER_ID).thenApply { server -> diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/ElementTypes.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/ElementTypes.kt index bbf0b32ae..820f1fe99 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/ElementTypes.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/ElementTypes.kt @@ -9,7 +9,11 @@ import org.jetbrains.plugins.textmate.language.syntax.lexer.TextMateScope val FILE = IStubFileElementType>("RobotFrameworkFile", RobotFrameworkLanguage) -class IRobotFrameworkElementType(debugName: String) : IElementType(debugName, RobotFrameworkLanguage) +open class IRobotFrameworkElementType(debugName: String, register: Boolean = true) : IElementType( + debugName, + RobotFrameworkLanguage, + register +) val HEADER = IRobotFrameworkElementType("HEADER") val SETTING = IRobotFrameworkElementType("SETTING") @@ -41,12 +45,12 @@ val COMMENT_TOKENS = TokenSet.create(COMMENT_LINE, COMMENT_BLOCK) val STRING_TOKENS = TokenSet.create(ARGUMENT) -class RobotTextMateElementType private constructor ( +class RobotTextMateElementType private constructor( val scope: TextMateScope, debugName: String = "ROBOT_TEXTMATE_ELEMENT_TYPE(${scope.scopeName})", register: Boolean = false -) : IElementType( - debugName, RobotFrameworkLanguage, register +) : IRobotFrameworkElementType( + debugName, register ) { override fun toString(): String { return "RobotTextMateElementType($scope)" diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/Elements.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/Elements.kt index a81c265f1..285167e6e 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/Elements.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/psi/Elements.kt @@ -4,8 +4,6 @@ import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.impl.source.tree.CompositePsiElement -import com.intellij.psi.tree.IElementType open class SimpleASTWrapperPsiElement(tcNode: ASTNode) : ASTWrapperPsiElement(tcNode) { override fun getChildren(): Array { diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeColorSettingsPage.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeColorSettingsPage.kt index 13ac65613..0bae28f85 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeColorSettingsPage.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/settings/RobotCodeColorSettingsPage.kt @@ -6,8 +6,8 @@ import com.intellij.openapi.options.colors.AttributesDescriptor import com.intellij.openapi.options.colors.ColorDescriptor import com.intellij.openapi.options.colors.ColorSettingsPage import dev.robotcode.robotcode4ij.RobotIcons -import dev.robotcode.robotcode4ij.highlighting.RobotCodeSyntaxHighlighter import dev.robotcode.robotcode4ij.highlighting.Colors +import dev.robotcode.robotcode4ij.highlighting.RobotCodeSyntaxHighlighter import javax.swing.Icon class RobotCodeColorSettingsPage : ColorSettingsPage { diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestActionProvider.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestActionProvider.kt new file mode 100644 index 000000000..f34950187 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestActionProvider.kt @@ -0,0 +1,35 @@ +package dev.robotcode.robotcode4ij.testing + +import com.intellij.execution.testframework.TestConsoleProperties +import com.intellij.execution.testframework.TestFrameworkRunningModel +import com.intellij.execution.testframework.ToggleModelAction +import com.intellij.execution.testframework.ToggleModelActionProvider +import com.intellij.icons.AllIcons +import com.intellij.util.config.BooleanProperty + +class RobotCodeTestActionProvider : ToggleModelActionProvider { + override fun createToggleModelAction(properties: TestConsoleProperties?): ToggleModelAction? { + // TODO: Implement this method + return RobotCodeToggleModelAction(properties) + } +} + +class RobotCodeToggleModelAction(properties: TestConsoleProperties?) : ToggleModelAction( + "Toggle Something", + "Description of Toggle Something", + AllIcons.RunConfigurations.Application, + properties, + BooleanProperty("Something", false) +) { + + var myModel: TestFrameworkRunningModel? = null + + override fun setModel(model: TestFrameworkRunningModel?) { + this.myModel = model + } + + override fun isEnabled(): Boolean { + + return true + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestManager.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestManager.kt index d11d257c9..d265ca39f 100644 --- a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestManager.kt +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestManager.kt @@ -7,10 +7,19 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.util.elementType +import com.intellij.psi.util.startOffset +import com.intellij.util.io.URLUtil import dev.robotcode.robotcode4ij.buildRobotCodeCommandLine +import dev.robotcode.robotcode4ij.psi.IRobotFrameworkElementType +import dev.robotcode.robotcode4ij.psi.RobotSuiteFile import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import java.net.URI @Serializable data class Position(val line: UInt, val character: UInt) @@ -107,8 +116,7 @@ import kotlinx.serialization.json.JsonElement var testItems: Array = arrayOf() private set - fun refresh() { - // TODO: Add support for configurable paths + fun refresh() { // TODO: Add support for configurable paths val defaultPaths = arrayOf("--default-path", ".") try { @@ -127,8 +135,78 @@ import kotlinx.serialization.json.JsonElement thisLogger().warn("Failed to discover test items", e) } } + + fun findTestItem( + uri: String, + line: UInt? = null, + ): RobotCodeTestItem? { + return findTestItem(testItems, uri, line) + } + + fun findTestItem( + root: RobotCodeTestItem, + uri: String, + line: UInt? = null, + ): RobotCodeTestItem? { + + if (line == null) { + if (root.uri == uri) { + return root + } + } else { + if (root.uri == uri && root.range != null && root.range.start.line == line) { + return root + } + } + + return findTestItem(root.children ?: arrayOf(), uri, line) + } + + fun findTestItem( + testItems: Array, uri: String, line: UInt? = null + ): RobotCodeTestItem? { + testItems.forEach { item -> + val found = findTestItem(item, uri, line) + if (found != null) { + return found + } + } + + return null + } + + + fun findTestItem(element: PsiElement): RobotCodeTestItem? { + val containingFile = element.containingFile ?: return null + if (containingFile !is RobotSuiteFile) { + return null + } + + if (element is RobotSuiteFile) { + val result = findTestItem(containingFile.virtualFile.uri) + return result + } + + if (element.elementType !is IRobotFrameworkElementType) { + return null + } + + val psiDocumentManager = PsiDocumentManager.getInstance(project) ?: return null + val document = psiDocumentManager.getDocument(containingFile) ?: return null + val lineNumber = document.getLineNumber(element.startOffset) + val columnNumber = element.startOffset - document.getLineStartOffset(lineNumber) + if (columnNumber != 0) return null + + val result = findTestItem(containingFile.virtualFile.uri, lineNumber.toUInt()) + return result + } } +val VirtualFile.uri: String + get() { + return URI.create(fileSystem.protocol + URLUtil.SCHEME_SEPARATOR + "/" + path.replace(":", "%3A")).toString() + } + val Project.testManger: RobotCodeTestManager get() { return this.service() diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestStatusListener.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestStatusListener.kt new file mode 100644 index 000000000..12c24c908 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/testing/RobotCodeTestStatusListener.kt @@ -0,0 +1,12 @@ +package dev.robotcode.robotcode4ij.testing + +import com.intellij.execution.testframework.AbstractTestProxy +import com.intellij.execution.testframework.TestStatusListener +import com.intellij.openapi.diagnostic.thisLogger + +class RobotCodeTestStatusListener : TestStatusListener() { + override fun testSuiteFinished(root: AbstractTestProxy?) { + thisLogger().info("Test suite finished: $root") + // TODO: Implement this method + } +} diff --git a/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/utils/NetUtils.kt b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/utils/NetUtils.kt new file mode 100644 index 000000000..e6615f197 --- /dev/null +++ b/intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/utils/NetUtils.kt @@ -0,0 +1,27 @@ +package dev.robotcode.robotcode4ij.utils + +import java.net.ServerSocket + +object NetUtils { + fun findFreePort(startPort: Int, endPort: Int? = null): Int { + + try { + ServerSocket(startPort).use { return startPort } + } catch (_: Exception) { + + } + + return if (endPort == null) { + + ServerSocket(0).use { it.localPort } + } else { + (startPort..endPort).firstOrNull { port -> + try { + ServerSocket(port).use { true } + } catch (e: Exception) { + false + } + } ?: ServerSocket(0).use { it.localPort } + } + } +} diff --git a/intellij-client/src/main/resources/META-INF/plugin.xml b/intellij-client/src/main/resources/META-INF/plugin.xml index ffffef10a..743473dba 100644 --- a/intellij-client/src/main/resources/META-INF/plugin.xml +++ b/intellij-client/src/main/resources/META-INF/plugin.xml @@ -28,7 +28,7 @@ - + @@ -72,17 +72,20 @@ - + - + implementation="dev.robotcode.robotcode4ij.execution.RobotCodeRunConfigurationProducer"/> + + + + + + + @@ -107,6 +110,15 @@ class="dev.robotcode.robotcode4ij.actions.RobotCreateFileAction"> + + + + + + + + + diff --git a/packages/debugger/src/robotcode/debugger/dap_types.py b/packages/debugger/src/robotcode/debugger/dap_types.py index 5455ec18c..b75103a4b 100644 --- a/packages/debugger/src/robotcode/debugger/dap_types.py +++ b/packages/debugger/src/robotcode/debugger/dap_types.py @@ -523,6 +523,9 @@ class SourceBreakpoint(Model): hit_condition: Optional[str] = None log_message: Optional[str] = None + def __hash__(self) -> int: + return hash((self.line, self.column, self.condition, self.hit_condition, self.log_message)) + @dataclass class SetBreakpointsArguments(Model): diff --git a/packages/debugger/src/robotcode/debugger/debugger.py b/packages/debugger/src/robotcode/debugger/debugger.py index 9a260ed65..327694bbf 100644 --- a/packages/debugger/src/robotcode/debugger/debugger.py +++ b/packages/debugger/src/robotcode/debugger/debugger.py @@ -8,6 +8,7 @@ import weakref from collections import deque from enum import Enum +from functools import cached_property from pathlib import Path, PurePath from typing import ( Any, @@ -67,6 +68,7 @@ Variable, VariablePresentationHint, ) +from .id_manager import IdManager if get_robot_version() >= (7, 0): from robot.running import UserKeyword as UserKeywordHandler @@ -163,6 +165,10 @@ def __init__(self, thread_id: Any) -> None: super().__init__(f"Invalid thread id {thread_id}") +class MarkerObject: + pass + + class StackFrameEntry: def __init__( self, @@ -193,10 +199,10 @@ def __init__( self.libname = libname self.kwname = kwname self.longname = longname - self._suite_marker = object() - self._test_marker = object() - self._local_marker = object() - self._global_marker = object() + self._suite_marker = MarkerObject() + self._test_marker = MarkerObject() + self._local_marker = MarkerObject() + self._global_marker = MarkerObject() self.stack_frames: Deque[StackFrameEntry] = deque() def __repr__(self) -> str: @@ -207,21 +213,27 @@ def get_first_or_self(self) -> "StackFrameEntry": return self.stack_frames[0] return self - @property + _id_manager = IdManager() + + @cached_property def id(self) -> int: - return id(self) + return self._id_manager.get_id(self) + @cached_property def test_id(self) -> int: - return id(self._test_marker) + return self._id_manager.get_id(self._test_marker) + @cached_property def suite_id(self) -> int: - return id(self._suite_marker) + return self._id_manager.get_id(self._suite_marker) + @cached_property def local_id(self) -> int: - return id(self._local_marker) + return self._id_manager.get_id(self._local_marker) + @cached_property def global_id(self) -> int: - return id(self._global_marker) + return self._id_manager.get_id(self._global_marker) class HitCountEntry(NamedTuple): @@ -268,6 +280,9 @@ def end_keyword(self, data: running.Keyword, result: result.Keyword) -> None: self.steps.pop() +breakpoint_id_manager = IdManager() + + class Debugger: __instance: ClassVar[Optional["Debugger"]] = None __lock: ClassVar = threading.RLock() @@ -521,6 +536,7 @@ def set_breakpoints( lines: Optional[List[int]] = None, source_modified: Optional[bool] = None, ) -> List[Breakpoint]: + if self.is_windows_path(source.path or ""): path: pathlib.PurePath = pathlib.PureWindowsPath(source.path or "") else: @@ -535,7 +551,7 @@ def set_breakpoints( ) return [ Breakpoint( - id=id(v), + id=breakpoint_id_manager.get_id(v), source=Source(path=str(path)), verified=True, line=v.line, @@ -559,6 +575,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str) self, StoppedEvent( body=StoppedEventBody( + description="Paused", reason=StoppedReason.PAUSE, thread_id=threading.current_thread().ident, ) @@ -574,6 +591,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str) self, StoppedEvent( body=StoppedEventBody( + description="Next step", reason=StoppedReason.STEP, thread_id=threading.current_thread().ident, ) @@ -588,6 +606,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str) self, StoppedEvent( body=StoppedEventBody( + description="Step in", reason=StoppedReason.STEP, thread_id=threading.current_thread().ident, ) @@ -602,6 +621,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str) self, StoppedEvent( body=StoppedEventBody( + description="Step out", reason=StoppedReason.STEP, thread_id=threading.current_thread().ident, ) @@ -613,6 +633,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str) if source_path in self.breakpoints: breakpoints = [v for v in self.breakpoints[source_path].breakpoints if v.line == line_no] if len(breakpoints) > 0: + for point in breakpoints: if point.condition is not None: hit = False @@ -673,9 +694,10 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str) self, StoppedEvent( body=StoppedEventBody( + description="Breakpoint hit", reason=StoppedReason.BREAKPOINT, thread_id=threading.current_thread().ident, - hit_breakpoint_ids=[id(v) for v in breakpoints], + hit_breakpoint_ids=[breakpoint_id_manager.get_id(v) for v in breakpoints], ) ), ) @@ -1346,7 +1368,7 @@ def get_scopes(self, frame_id: int) -> List[Scope]: name="Local", expensive=False, presentation_hint="local", - variables_reference=entry.local_id(), + variables_reference=entry.local_id, ) ) if context.variables._test is not None and entry.type == "KEYWORD": @@ -1355,7 +1377,7 @@ def get_scopes(self, frame_id: int) -> List[Scope]: name="Test", expensive=False, presentation_hint="test", - variables_reference=entry.test_id(), + variables_reference=entry.test_id, ) ) if context.variables._suite is not None and entry.type in [ @@ -1367,7 +1389,7 @@ def get_scopes(self, frame_id: int) -> List[Scope]: name="Suite", expensive=False, presentation_hint="suite", - variables_reference=entry.suite_id(), + variables_reference=entry.suite_id, ) ) if context.variables._global is not None: @@ -1376,16 +1398,18 @@ def get_scopes(self, frame_id: int) -> List[Scope]: name="Global", expensive=False, presentation_hint="global", - variables_reference=entry.global_id(), + variables_reference=entry.global_id, ) ) return result + _cache_id_manager = IdManager() + def _new_cache_id(self) -> int: - o = object() + o = MarkerObject() self._variables_object_cache.append(o) - return id(o) + return StackFrameEntry._id_manager.get_id(o) debug_repr = DebugRepr() @@ -1443,18 +1467,18 @@ def get_variables( ( v for v in self.stack_frames - if variables_reference in [v.global_id(), v.suite_id(), v.test_id(), v.local_id()] + if variables_reference in [v.global_id, v.suite_id, v.test_id, v.local_id] ), None, ) if entry is not None: context = entry.context() if context is not None: - if entry.global_id() == variables_reference: + if entry.global_id == variables_reference: result.update( {k: self._create_variable(k, v) for k, v in context.variables._global.as_dict().items()} ) - elif entry.suite_id() == variables_reference: + elif entry.suite_id == variables_reference: globals = context.variables._global.as_dict() vars = entry.get_first_or_self().variables() vars_dict = vars.as_dict() if vars is not None else {} @@ -1465,7 +1489,7 @@ def get_variables( if (k not in globals or globals[k] != v) and (k in vars_dict) } ) - elif entry.test_id() == variables_reference: + elif entry.test_id == variables_reference: globals = context.variables._suite.as_dict() vars = entry.get_first_or_self().variables() vars_dict = vars.as_dict() if vars is not None else {} @@ -1476,7 +1500,7 @@ def get_variables( if (k not in globals or globals[k] != v) and (k in vars_dict) } ) - elif entry.local_id() == variables_reference: + elif entry.local_id == variables_reference: vars = entry.get_first_or_self().variables() if vars is not None: p = entry.parent() if entry.parent else None @@ -1821,7 +1845,7 @@ def set_variable( ( v for v in self.full_stack_frames - if variables_reference in [v.global_id(), v.local_id(), v.suite_id(), v.test_id()] + if variables_reference in [v.global_id, v.local_id, v.suite_id, v.test_id] ), None, ) diff --git a/packages/debugger/src/robotcode/debugger/id_manager.py b/packages/debugger/src/robotcode/debugger/id_manager.py new file mode 100644 index 000000000..5fa49b2e7 --- /dev/null +++ b/packages/debugger/src/robotcode/debugger/id_manager.py @@ -0,0 +1,64 @@ +import weakref +from collections import deque +from typing import Any, Deque, Dict, Optional + + +class IdManager: + def __init__(self) -> None: + self.max_id: int = 2**31 - 1 + self.next_id: int = 0 + + self.released_ids: Deque[int] = deque() + self.object_to_id: weakref.WeakKeyDictionary[Any, int] = weakref.WeakKeyDictionary() + self.id_to_object: weakref.WeakValueDictionary[int, Any] = weakref.WeakValueDictionary() + self._finalizers: Dict[int, weakref.ref[Any]] = {} + + def get_id(self, obj: Any) -> int: + if obj in self.object_to_id: + return self.object_to_id[obj] + + if self.released_ids: + obj_id: int = self.released_ids.popleft() + else: + if self.next_id > self.max_id: + raise RuntimeError("Keine IDs mehr verfügbar!") + obj_id = self.next_id + self.next_id += 1 + + self.object_to_id[obj] = obj_id + self.id_to_object[obj_id] = obj + + def _on_object_gc(ref: "weakref.ReferenceType[Any]", id_: int = obj_id) -> None: + self.release_id(id_) + + ref = weakref.ref(obj, _on_object_gc) + self._finalizers[obj_id] = ref + + return obj_id + + def release_id(self, obj_id: int) -> None: + if obj_id in self.id_to_object: + del self.id_to_object[obj_id] + + if obj_id in self._finalizers: + del self._finalizers[obj_id] + + self.released_ids.append(obj_id) + + def release_obj(self, obj: Any) -> None: + if obj in self.object_to_id: + obj_id: int = self.object_to_id.pop(obj) + + if obj_id in self.id_to_object: + del self.id_to_object[obj_id] + + if obj_id in self._finalizers: + del self._finalizers[obj_id] + + self.released_ids.append(obj_id) + + def get_object(self, obj_id: int) -> Optional[Any]: + return self.id_to_object.get(obj_id) + + def get_id_from_obj(self, obj: Any) -> Optional[int]: + return self.object_to_id.get(obj) diff --git a/packages/debugger/src/robotcode/debugger/listeners.py b/packages/debugger/src/robotcode/debugger/listeners.py index db5b7e750..b52f06e5d 100644 --- a/packages/debugger/src/robotcode/debugger/listeners.py +++ b/packages/debugger/src/robotcode/debugger/listeners.py @@ -16,6 +16,8 @@ class RobotExecutionEventBody(Model): type: str id: str + name: str + parent_id: Optional[str] = None attributes: Optional[Dict[str, Any]] = None failed_keywords: Optional[List[Dict[str, Any]]] = None @@ -34,15 +36,19 @@ class ListenerV2: def __init__(self) -> None: self.failed_keywords: Optional[List[Dict[str, Any]]] = None self.last_fail_message: Optional[str] = None + self.suite_id_stack: List[str] = [] def start_suite(self, name: str, attributes: Dict[str, Any]) -> None: + id = f"{source_from_attributes(attributes)};{attributes.get('longname', '')}" Debugger.instance().send_event( self, Event( event="robotStarted", body=RobotExecutionEventBody( type="suite", - id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')}", + name=name, + id=id, + parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None, attributes=dict(attributes), ), ), @@ -51,8 +57,10 @@ def start_suite(self, name: str, attributes: Dict[str, Any]) -> None: Debugger.instance().start_output_group(name, attributes, "SUITE") Debugger.instance().start_suite(name, attributes) + self.suite_id_stack.append(id) def end_suite(self, name: str, attributes: Dict[str, Any]) -> None: + id = f"{source_from_attributes(attributes)};{attributes.get('longname', '')}" Debugger.instance().end_suite(name, attributes) Debugger.instance().end_output_group(name, attributes, "SUITE") @@ -63,13 +71,15 @@ def end_suite(self, name: str, attributes: Dict[str, Any]) -> None: event="robotEnded", body=RobotExecutionEventBody( type="suite", + name=name, attributes=dict(attributes), - id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')}", + id=id, + parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None, failed_keywords=self.failed_keywords, ), ), ) - + self.suite_id_stack.pop() self.failed_keywords = None def start_test(self, name: str, attributes: Dict[str, Any]) -> None: @@ -81,8 +91,10 @@ def start_test(self, name: str, attributes: Dict[str, Any]) -> None: event="robotStarted", body=RobotExecutionEventBody( type="test", + name=name, id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')};" f"{attributes.get('lineno', 0)}", + parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None, attributes=dict(attributes), ), ), @@ -103,8 +115,10 @@ def end_test(self, name: str, attributes: Dict[str, Any]) -> None: event="robotEnded", body=RobotExecutionEventBody( type="test", + name=name, id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')};" f"{attributes.get('lineno', 0)}", + parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None, attributes=dict(attributes), failed_keywords=self.failed_keywords, ), @@ -308,6 +322,7 @@ def report_status( event="robotSetFailed", body=RobotExecutionEventBody( type="test", + name=result_item.name, attributes={ "longname": result_item.longname, "status": str(result_item.status), @@ -325,6 +340,7 @@ def report_status( ), ), ) + if isinstance(result_item, result.TestSuite): for r in result_item.suites: p = next((i for i in data_item.suites if i.id == r.id), None) if data_item else None diff --git a/packages/debugger/src/robotcode/debugger/server.py b/packages/debugger/src/robotcode/debugger/server.py index 5439b7d5b..a6d7d71bd 100644 --- a/packages/debugger/src/robotcode/debugger/server.py +++ b/packages/debugger/src/robotcode/debugger/server.py @@ -132,7 +132,7 @@ def wait_for_initialized(self, timeout: float = 30) -> bool: return self._initialized @_logger.call - def wait_for_disconnected(self, timeout: float = 15) -> bool: + def wait_for_disconnected(self, timeout: float = 1) -> bool: self._disconnected_event.wait(timeout) return not self._connected diff --git a/packages/runner/src/robotcode/runner/cli/discover/discover.py b/packages/runner/src/robotcode/runner/cli/discover/discover.py index d37023fc0..214ef0e3d 100644 --- a/packages/runner/src/robotcode/runner/cli/discover/discover.py +++ b/packages/runner/src/robotcode/runner/cli/discover/discover.py @@ -234,7 +234,7 @@ class Statistics(CamelSnakeMixin): tasks: int = 0 -def get_rel_source(source: Optional[str]) -> Optional[str]: +def get_rel_source(source: Union[str, Path, None]) -> Optional[str]: if source is None: return None try: @@ -253,6 +253,8 @@ def __init__(self) -> None: name=absolute_path.name, longname=absolute_path.name, uri=str(Uri.from_path(absolute_path)), + source=str(absolute_path), + rel_source=get_rel_source(absolute_path), needs_parse_include=get_robot_version() >= (6, 1), ) self._current = self.all diff --git a/vscode-client/extension/debugmanager.ts b/vscode-client/extension/debugmanager.ts index bf9a80c12..1fc6492bb 100644 --- a/vscode-client/extension/debugmanager.ts +++ b/vscode-client/extension/debugmanager.ts @@ -274,7 +274,7 @@ class RobotCodeDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescr ]); while (!(await isPortOpen(port, host))) { - await sleep(1000); + await sleep(5000); } try { diff --git a/vscode-client/extension/testcontrollermanager.ts b/vscode-client/extension/testcontrollermanager.ts index 3a647fb78..468c3e0b8 100644 --- a/vscode-client/extension/testcontrollermanager.ts +++ b/vscode-client/extension/testcontrollermanager.ts @@ -59,7 +59,6 @@ interface RobotCodeProfilesResult { interface RobotExecutionAttributes { id: string | undefined; longname: string | undefined; - originalname: string | undefined; template: string | undefined; status: string | undefined; message: string | undefined;